From 3d73254c41c2e447dd3dd4354429f96d5fee80ba Mon Sep 17 00:00:00 2001 From: Michael Reimsbach Date: Tue, 2 Jul 2024 09:53:36 +0200 Subject: [PATCH] feat(issue): create mutations --- .../api/graphql/graph/baseResolver/issue.go | 4 +- internal/api/graphql/graph/generated.go | 433 +++++++++++++++++- internal/api/graphql/graph/model/models.go | 26 +- .../api/graphql/graph/model/models_gen.go | 10 +- .../queryCollection/issue/create.graphql | 13 + .../queryCollection/issue/delete.graphql | 8 + .../queryCollection/issue/update.graphql | 14 + .../api/graphql/graph/schema/issue.graphqls | 7 + .../graphql/graph/schema/mutation.graphqls | 4 + internal/app/interface.go | 4 + internal/app/issue.go | 76 +++ internal/app/issue_test.go | 112 +++++ internal/database/interface.go | 3 + internal/database/mariadb/entity.go | 25 + internal/database/mariadb/issue.go | 96 ++++ internal/database/mariadb/issue_test.go | 114 +++++ internal/e2e/issue_query_test.go | 197 ++++++++ internal/entity/test/issue.go | 7 +- internal/mocks/mock_Database.go | 153 ++++++- internal/mocks/mock_Heureka.go | 165 ++++++- 20 files changed, 1455 insertions(+), 16 deletions(-) create mode 100644 internal/api/graphql/graph/queryCollection/issue/create.graphql create mode 100644 internal/api/graphql/graph/queryCollection/issue/delete.graphql create mode 100644 internal/api/graphql/graph/queryCollection/issue/update.graphql diff --git a/internal/api/graphql/graph/baseResolver/issue.go b/internal/api/graphql/graph/baseResolver/issue.go index ad08b858..d5e2c870 100644 --- a/internal/api/graphql/graph/baseResolver/issue.go +++ b/internal/api/graphql/graph/baseResolver/issue.go @@ -49,7 +49,7 @@ func SingleIssueBaseResolver(app app.Heureka, ctx context.Context, parent *model } var ir entity.IssueResult = issues.Elements[0] - issue := model.NewIssue(&ir) + issue := model.NewIssueWithAggregations(&ir) return &issue, nil } @@ -113,7 +113,7 @@ func IssueBaseResolver(app app.Heureka, ctx context.Context, filter *model.Issue edges := []*model.IssueEdge{} for _, result := range issues.Elements { - iss := model.NewIssue(&result) + iss := model.NewIssueWithAggregations(&result) edge := model.IssueEdge{ Node: &iss, Cursor: result.Cursor(), diff --git a/internal/api/graphql/graph/generated.go b/internal/api/graphql/graph/generated.go index 9ea06244..f5e70df5 100644 --- a/internal/api/graphql/graph/generated.go +++ b/internal/api/graphql/graph/generated.go @@ -1,6 +1,3 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors -// SPDX-License-Identifier: Apache-2.0 - // Code generated by github.com/99designs/gqlgen, DO NOT EDIT. package graph @@ -217,6 +214,7 @@ type ComplexityRoot struct { Issue struct { Activities func(childComplexity int, filter *model.ActivityFilter, first *int, after *string) int ComponentVersions func(childComplexity int, filter *model.ComponentVersionFilter, first *int, after *string) int + Description func(childComplexity int) int ID func(childComplexity int) int IssueMatches func(childComplexity int, filter *model.IssueMatchFilter, first *int, after *string) int IssueVariants func(childComplexity int, filter *model.IssueVariantFilter, first *int, after *string) int @@ -352,6 +350,7 @@ type ComplexityRoot struct { CreateComponentInstance func(childComplexity int, input model.ComponentInstanceInput) int CreateComponentVersion func(childComplexity int, input model.ComponentVersionInput) int CreateEvidence func(childComplexity int, input model.EvidenceInput) int + CreateIssue func(childComplexity int, input model.IssueInput) int CreateIssueMatch func(childComplexity int, input model.IssueMatchInput) int CreateIssueRepository func(childComplexity int, input model.IssueRepositoryInput) int CreateIssueVariant func(childComplexity int, input model.IssueVariantInput) int @@ -363,6 +362,7 @@ type ComplexityRoot struct { DeleteComponentInstance func(childComplexity int, id string) int DeleteComponentVersion func(childComplexity int, id string) int DeleteEvidence func(childComplexity int, id string) int + DeleteIssue func(childComplexity int, id string) int DeleteIssueMatch func(childComplexity int, id string) int DeleteIssueRepository func(childComplexity int, id string) int DeleteIssueVariant func(childComplexity int, id string) int @@ -374,6 +374,7 @@ type ComplexityRoot struct { UpdateComponentInstance func(childComplexity int, id string, input model.ComponentInstanceInput) int UpdateComponentVersion func(childComplexity int, id string, input model.ComponentVersionInput) int UpdateEvidence func(childComplexity int, id string, input model.EvidenceInput) int + UpdateIssue func(childComplexity int, id string, input model.IssueInput) int UpdateIssueMatch func(childComplexity int, id string, input model.IssueMatchInput) int UpdateIssueRepository func(childComplexity int, id string, input model.IssueRepositoryInput) int UpdateIssueVariant func(childComplexity int, id string, input model.IssueVariantInput) int @@ -557,6 +558,9 @@ type MutationResolver interface { CreateIssueRepository(ctx context.Context, input model.IssueRepositoryInput) (*model.IssueRepository, error) UpdateIssueRepository(ctx context.Context, id string, input model.IssueRepositoryInput) (*model.IssueRepository, error) DeleteIssueRepository(ctx context.Context, id string) (string, error) + CreateIssue(ctx context.Context, input model.IssueInput) (*model.Issue, error) + UpdateIssue(ctx context.Context, id string, input model.IssueInput) (*model.Issue, error) + DeleteIssue(ctx context.Context, id string) (string, error) CreateIssueVariant(ctx context.Context, input model.IssueVariantInput) (*model.IssueVariant, error) UpdateIssueVariant(ctx context.Context, id string, input model.IssueVariantInput) (*model.IssueVariant, error) DeleteIssueVariant(ctx context.Context, id string) (string, error) @@ -1321,6 +1325,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Issue.ComponentVersions(childComplexity, args["filter"].(*model.ComponentVersionFilter), args["first"].(*int), args["after"].(*string)), true + case "Issue.description": + if e.complexity.Issue.Description == nil { + break + } + + return e.complexity.Issue.Description(childComplexity), true + case "Issue.id": if e.complexity.Issue.ID == nil { break @@ -1990,6 +2001,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.CreateEvidence(childComplexity, args["input"].(model.EvidenceInput)), true + case "Mutation.createIssue": + if e.complexity.Mutation.CreateIssue == nil { + break + } + + args, err := ec.field_Mutation_createIssue_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.CreateIssue(childComplexity, args["input"].(model.IssueInput)), true + case "Mutation.createIssueMatch": if e.complexity.Mutation.CreateIssueMatch == nil { break @@ -2122,6 +2145,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.DeleteEvidence(childComplexity, args["id"].(string)), true + case "Mutation.deleteIssue": + if e.complexity.Mutation.DeleteIssue == nil { + break + } + + args, err := ec.field_Mutation_deleteIssue_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.DeleteIssue(childComplexity, args["id"].(string)), true + case "Mutation.deleteIssueMatch": if e.complexity.Mutation.DeleteIssueMatch == nil { break @@ -2254,6 +2289,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.UpdateEvidence(childComplexity, args["id"].(string), args["input"].(model.EvidenceInput)), true + case "Mutation.updateIssue": + if e.complexity.Mutation.UpdateIssue == nil { + break + } + + args, err := ec.field_Mutation_updateIssue_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.UpdateIssue(childComplexity, args["id"].(string), args["input"].(model.IssueInput)), true + case "Mutation.updateIssueMatch": if e.complexity.Mutation.UpdateIssueMatch == nil { break @@ -2862,6 +2909,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec.unmarshalInputEvidenceFilter, ec.unmarshalInputEvidenceInput, ec.unmarshalInputIssueFilter, + ec.unmarshalInputIssueInput, ec.unmarshalInputIssueMatchChangeFilter, ec.unmarshalInputIssueMatchFilter, ec.unmarshalInputIssueMatchInput, @@ -3679,6 +3727,21 @@ func (ec *executionContext) field_Mutation_createIssueVariant_args(ctx context.C return args, nil } +func (ec *executionContext) field_Mutation_createIssue_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 model.IssueInput + if tmp, ok := rawArgs["input"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("input")) + arg0, err = ec.unmarshalNIssueInput2githubᚗwdfᚗsapᚗcorpᚋccᚋheurekaᚋinternalᚋapiᚋgraphqlᚋgraphᚋmodelᚐIssueInput(ctx, tmp) + if err != nil { + return nil, err + } + } + args["input"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation_createService_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -3844,6 +3907,21 @@ func (ec *executionContext) field_Mutation_deleteIssueVariant_args(ctx context.C return args, nil } +func (ec *executionContext) field_Mutation_deleteIssue_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 string + if tmp, ok := rawArgs["id"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("id")) + arg0, err = ec.unmarshalNID2string(ctx, tmp) + if err != nil { + return nil, err + } + } + args["id"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation_deleteService_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -4081,6 +4159,30 @@ func (ec *executionContext) field_Mutation_updateIssueVariant_args(ctx context.C return args, nil } +func (ec *executionContext) field_Mutation_updateIssue_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 string + if tmp, ok := rawArgs["id"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("id")) + arg0, err = ec.unmarshalNID2string(ctx, tmp) + if err != nil { + return nil, err + } + } + args["id"] = arg0 + var arg1 model.IssueInput + if tmp, ok := rawArgs["input"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("input")) + arg1, err = ec.unmarshalNIssueInput2githubᚗwdfᚗsapᚗcorpᚋccᚋheurekaᚋinternalᚋapiᚋgraphqlᚋgraphᚋmodelᚐIssueInput(ctx, tmp) + if err != nil { + return nil, err + } + } + args["input"] = arg1 + return args, nil +} + func (ec *executionContext) field_Mutation_updateService_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -9292,6 +9394,47 @@ func (ec *executionContext) fieldContext_Issue_primaryName(_ context.Context, fi return fc, nil } +func (ec *executionContext) _Issue_description(ctx context.Context, field graphql.CollectedField, obj *model.Issue) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Issue_description(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Description, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Issue_description(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Issue", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _Issue_lastModified(ctx context.Context, field graphql.CollectedField, obj *model.Issue) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Issue_lastModified(ctx, field) if err != nil { @@ -9824,6 +9967,8 @@ func (ec *executionContext) fieldContext_IssueEdge_node(_ context.Context, field return ec.fieldContext_Issue_type(ctx, field) case "primaryName": return ec.fieldContext_Issue_primaryName(ctx, field) + case "description": + return ec.fieldContext_Issue_description(ctx, field) case "lastModified": return ec.fieldContext_Issue_lastModified(ctx, field) case "issueVariants": @@ -10347,6 +10492,8 @@ func (ec *executionContext) fieldContext_IssueMatch_issue(_ context.Context, fie return ec.fieldContext_Issue_type(ctx, field) case "primaryName": return ec.fieldContext_Issue_primaryName(ctx, field) + case "description": + return ec.fieldContext_Issue_description(ctx, field) case "lastModified": return ec.fieldContext_Issue_lastModified(ctx, field) case "issueVariants": @@ -12804,6 +12951,8 @@ func (ec *executionContext) fieldContext_IssueVariant_issue(_ context.Context, f return ec.fieldContext_Issue_type(ctx, field) case "primaryName": return ec.fieldContext_Issue_primaryName(ctx, field) + case "description": + return ec.fieldContext_Issue_description(ctx, field) case "lastModified": return ec.fieldContext_Issue_lastModified(ctx, field) case "issueVariants": @@ -14595,6 +14744,215 @@ func (ec *executionContext) fieldContext_Mutation_deleteIssueRepository(ctx cont return fc, nil } +func (ec *executionContext) _Mutation_createIssue(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_createIssue(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().CreateIssue(rctx, fc.Args["input"].(model.IssueInput)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*model.Issue) + fc.Result = res + return ec.marshalNIssue2ᚖgithubᚗwdfᚗsapᚗcorpᚋccᚋheurekaᚋinternalᚋapiᚋgraphqlᚋgraphᚋmodelᚐIssue(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Mutation_createIssue(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_Issue_id(ctx, field) + case "type": + return ec.fieldContext_Issue_type(ctx, field) + case "primaryName": + return ec.fieldContext_Issue_primaryName(ctx, field) + case "description": + return ec.fieldContext_Issue_description(ctx, field) + case "lastModified": + return ec.fieldContext_Issue_lastModified(ctx, field) + case "issueVariants": + return ec.fieldContext_Issue_issueVariants(ctx, field) + case "activities": + return ec.fieldContext_Issue_activities(ctx, field) + case "issueMatches": + return ec.fieldContext_Issue_issueMatches(ctx, field) + case "componentVersions": + return ec.fieldContext_Issue_componentVersions(ctx, field) + case "metadata": + return ec.fieldContext_Issue_metadata(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Issue", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_createIssue_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Mutation_updateIssue(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_updateIssue(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().UpdateIssue(rctx, fc.Args["id"].(string), fc.Args["input"].(model.IssueInput)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*model.Issue) + fc.Result = res + return ec.marshalNIssue2ᚖgithubᚗwdfᚗsapᚗcorpᚋccᚋheurekaᚋinternalᚋapiᚋgraphqlᚋgraphᚋmodelᚐIssue(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Mutation_updateIssue(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_Issue_id(ctx, field) + case "type": + return ec.fieldContext_Issue_type(ctx, field) + case "primaryName": + return ec.fieldContext_Issue_primaryName(ctx, field) + case "description": + return ec.fieldContext_Issue_description(ctx, field) + case "lastModified": + return ec.fieldContext_Issue_lastModified(ctx, field) + case "issueVariants": + return ec.fieldContext_Issue_issueVariants(ctx, field) + case "activities": + return ec.fieldContext_Issue_activities(ctx, field) + case "issueMatches": + return ec.fieldContext_Issue_issueMatches(ctx, field) + case "componentVersions": + return ec.fieldContext_Issue_componentVersions(ctx, field) + case "metadata": + return ec.fieldContext_Issue_metadata(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Issue", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_updateIssue_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Mutation_deleteIssue(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_deleteIssue(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().DeleteIssue(rctx, fc.Args["id"].(string)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Mutation_deleteIssue(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_deleteIssue_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _Mutation_createIssueVariant(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Mutation_createIssueVariant(ctx, field) if err != nil { @@ -20716,6 +21074,47 @@ func (ec *executionContext) unmarshalInputIssueFilter(ctx context.Context, obj i return it, nil } +func (ec *executionContext) unmarshalInputIssueInput(ctx context.Context, obj interface{}) (model.IssueInput, error) { + var it model.IssueInput + asMap := map[string]interface{}{} + for k, v := range obj.(map[string]interface{}) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"primaryName", "description", "type"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "primaryName": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("primaryName")) + data, err := ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.PrimaryName = data + case "description": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("description")) + data, err := ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.Description = data + case "type": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("type")) + data, err := ec.unmarshalOIssueTypes2ᚖgithubᚗwdfᚗsapᚗcorpᚋccᚋheurekaᚋinternalᚋapiᚋgraphqlᚋgraphᚋmodelᚐIssueTypes(ctx, v) + if err != nil { + return it, err + } + it.Type = data + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputIssueMatchChangeFilter(ctx context.Context, obj interface{}) (model.IssueMatchChangeFilter, error) { var it model.IssueMatchChangeFilter asMap := map[string]interface{}{} @@ -22892,6 +23291,8 @@ func (ec *executionContext) _Issue(ctx context.Context, sel ast.SelectionSet, ob out.Values[i] = ec._Issue_type(ctx, field, obj) case "primaryName": out.Values[i] = ec._Issue_primaryName(ctx, field, obj) + case "description": + out.Values[i] = ec._Issue_description(ctx, field, obj) case "lastModified": out.Values[i] = ec._Issue_lastModified(ctx, field, obj) case "issueVariants": @@ -24327,6 +24728,27 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { out.Invalids++ } + case "createIssue": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_createIssue(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "updateIssue": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_updateIssue(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "deleteIssue": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_deleteIssue(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } case "createIssueVariant": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { return ec._Mutation_createIssueVariant(ctx, field) @@ -26144,6 +26566,11 @@ func (ec *executionContext) marshalNIssueEdge2ᚕᚖgithubᚗwdfᚗsapᚗcorpᚋ return ret } +func (ec *executionContext) unmarshalNIssueInput2githubᚗwdfᚗsapᚗcorpᚋccᚋheurekaᚋinternalᚋapiᚋgraphqlᚋgraphᚋmodelᚐIssueInput(ctx context.Context, v interface{}) (model.IssueInput, error) { + res, err := ec.unmarshalInputIssueInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) marshalNIssueMatch2githubᚗwdfᚗsapᚗcorpᚋccᚋheurekaᚋinternalᚋapiᚋgraphqlᚋgraphᚋmodelᚐIssueMatch(ctx context.Context, sel ast.SelectionSet, v model.IssueMatch) graphql.Marshaler { return ec._IssueMatch(ctx, sel, &v) } diff --git a/internal/api/graphql/graph/model/models.go b/internal/api/graphql/graph/model/models.go index 55a3481a..cf784d0a 100644 --- a/internal/api/graphql/graph/model/models.go +++ b/internal/api/graphql/graph/model/models.go @@ -130,7 +130,19 @@ func NewSeverityEntity(severity *SeverityInput) entity.Severity { return entity.NewSeverity(*severity.Vector) } -func NewIssue(issue *entity.IssueResult) Issue { +func NewIssue(issue *entity.Issue) Issue { + lastModified := issue.UpdatedAt.String() + issueType := IssueTypes(issue.Type.String()) + return Issue{ + ID: fmt.Sprintf("%d", issue.Id), + PrimaryName: &issue.PrimaryName, + Type: &issueType, + Description: &issue.Description, + LastModified: &lastModified, + } +} + +func NewIssueWithAggregations(issue *entity.IssueResult) Issue { lastModified := issue.Issue.UpdatedAt.String() issueType := IssueTypes(issue.Type.String()) @@ -157,6 +169,18 @@ func NewIssue(issue *entity.IssueResult) Issue { } } +func NewIssueEntity(issue *IssueInput) entity.Issue { + issueType := "" + if issue.Type != nil && issue.Type.IsValid() { + issueType = issue.Type.String() + } + return entity.Issue{ + PrimaryName: lo.FromPtr(issue.PrimaryName), + Description: lo.FromPtr(issue.Description), + Type: entity.NewIssueType(issueType), + } +} + func NewIssueMatch(im *entity.IssueMatch) IssueMatch { status := IssueMatchStatusValue(im.Status.String()) targetRemediationDate := im.TargetRemediationDate.Format(time.RFC3339) diff --git a/internal/api/graphql/graph/model/models_gen.go b/internal/api/graphql/graph/model/models_gen.go index dc9364ff..9820f635 100644 --- a/internal/api/graphql/graph/model/models_gen.go +++ b/internal/api/graphql/graph/model/models_gen.go @@ -1,6 +1,3 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors -// SPDX-License-Identifier: Apache-2.0 - // Code generated by github.com/99designs/gqlgen, DO NOT EDIT. package model @@ -294,6 +291,7 @@ type Issue struct { ID string `json:"id"` Type *IssueTypes `json:"type,omitempty"` PrimaryName *string `json:"primaryName,omitempty"` + Description *string `json:"description,omitempty"` LastModified *string `json:"lastModified,omitempty"` IssueVariants *IssueVariantConnection `json:"issueVariants,omitempty"` Activities *ActivityConnection `json:"activities,omitempty"` @@ -332,6 +330,12 @@ type IssueFilter struct { ComponentVersionID []*string `json:"componentVersionId,omitempty"` } +type IssueInput struct { + PrimaryName *string `json:"primaryName,omitempty"` + Description *string `json:"description,omitempty"` + Type *IssueTypes `json:"type,omitempty"` +} + type IssueMatch struct { ID string `json:"id"` Status *IssueMatchStatusValues `json:"status,omitempty"` diff --git a/internal/api/graphql/graph/queryCollection/issue/create.graphql b/internal/api/graphql/graph/queryCollection/issue/create.graphql new file mode 100644 index 00000000..33383af3 --- /dev/null +++ b/internal/api/graphql/graph/queryCollection/issue/create.graphql @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +# SPDX-License-Identifier: Apache-2.0 + +mutation ($input: IssueInput!) { + createIssue ( + input: $input + ) { + id + primaryName + description + type + } +} \ No newline at end of file diff --git a/internal/api/graphql/graph/queryCollection/issue/delete.graphql b/internal/api/graphql/graph/queryCollection/issue/delete.graphql new file mode 100644 index 00000000..813f8981 --- /dev/null +++ b/internal/api/graphql/graph/queryCollection/issue/delete.graphql @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +# SPDX-License-Identifier: Apache-2.0 + +mutation ($id: ID!) { + deleteIssue ( + id: $id + ) +} \ No newline at end of file diff --git a/internal/api/graphql/graph/queryCollection/issue/update.graphql b/internal/api/graphql/graph/queryCollection/issue/update.graphql new file mode 100644 index 00000000..36a7be11 --- /dev/null +++ b/internal/api/graphql/graph/queryCollection/issue/update.graphql @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +# SPDX-License-Identifier: Apache-2.0 + +mutation ($id: ID!, $input: IssueInput!) { + updateIssue ( + id: $id, + input: $input + ) { + id + primaryName + description + type + } +} \ No newline at end of file diff --git a/internal/api/graphql/graph/schema/issue.graphqls b/internal/api/graphql/graph/schema/issue.graphqls index 4257425e..e5364fb5 100644 --- a/internal/api/graphql/graph/schema/issue.graphqls +++ b/internal/api/graphql/graph/schema/issue.graphqls @@ -7,6 +7,7 @@ type Issue implements Node { id: ID! type: IssueTypes primaryName: String + description: String lastModified: DateTime issueVariants(filter: IssueVariantFilter, first: Int, after: String): IssueVariantConnection activities(filter: ActivityFilter, first: Int, after: String): ActivityConnection @@ -25,6 +26,12 @@ type IssueMetadata { earliestTargetRemediationDate: DateTime! } +input IssueInput { + primaryName: String + description: String + type: IssueTypes +} + type IssueConnection implements Connection { totalCount: Int! edges: [IssueEdge]! diff --git a/internal/api/graphql/graph/schema/mutation.graphqls b/internal/api/graphql/graph/schema/mutation.graphqls index 96a55c73..9581993d 100644 --- a/internal/api/graphql/graph/schema/mutation.graphqls +++ b/internal/api/graphql/graph/schema/mutation.graphqls @@ -30,6 +30,10 @@ type Mutation { updateIssueRepository(id: ID!, input: IssueRepositoryInput!): IssueRepository! deleteIssueRepository(id: ID!): String! + createIssue(input: IssueInput!): Issue! + updateIssue(id: ID!, input: IssueInput!): Issue! + deleteIssue(id: ID!): String! + createIssueVariant(input: IssueVariantInput!): IssueVariant! updateIssueVariant(id: ID!, input: IssueVariantInput!): IssueVariant! deleteIssueVariant(id: ID!): String! diff --git a/internal/app/interface.go b/internal/app/interface.go index 39ff4e5c..2c9959dd 100644 --- a/internal/app/interface.go +++ b/internal/app/interface.go @@ -9,6 +9,10 @@ import ( type Heureka interface { ListIssues(*entity.IssueFilter, *entity.ListOptions) (*entity.List[entity.IssueResult], error) + CreateIssue(*entity.Issue) (*entity.Issue, error) + UpdateIssue(*entity.Issue) (*entity.Issue, error) + DeleteIssue(int64) error + ListIssueVariants(*entity.IssueVariantFilter, *entity.ListOptions) (*entity.List[entity.IssueVariantResult], error) CreateIssueVariant(*entity.IssueVariant) (*entity.IssueVariant, error) UpdateIssueVariant(*entity.IssueVariant) (*entity.IssueVariant, error) diff --git a/internal/app/issue.go b/internal/app/issue.go index 3c5dcb36..8959fa20 100644 --- a/internal/app/issue.go +++ b/internal/app/issue.go @@ -99,3 +99,79 @@ func (h *HeurekaApp) ListIssues(filter *entity.IssueFilter, options *entity.List Elements: res, }, nil } + +func (h *HeurekaApp) CreateIssue(issue *entity.Issue) (*entity.Issue, error) { + f := &entity.IssueFilter{ + PrimaryName: []*string{&issue.PrimaryName}, + } + + l := logrus.WithFields(logrus.Fields{ + "event": "app.CreateIssue", + "object": issue, + "filter": f, + }) + + issues, err := h.ListIssues(f, &entity.ListOptions{}) + + if err != nil { + l.Error(err) + return nil, heurekaError("Internal error while creating issue.") + } + + if len(issues.Elements) > 0 { + return nil, heurekaError(fmt.Sprintf("Duplicated entry %s for primaryName.", issue.PrimaryName)) + } + + newIssue, err := h.database.CreateIssue(issue) + + if err != nil { + l.Error(err) + return nil, heurekaError("Internal error while creating issue.") + } + + return newIssue, nil +} + +func (h *HeurekaApp) UpdateIssue(issue *entity.Issue) (*entity.Issue, error) { + l := logrus.WithFields(logrus.Fields{ + "event": "app.UpdateIssue", + "object": issue, + }) + + err := h.database.UpdateIssue(issue) + + if err != nil { + l.Error(err) + return nil, heurekaError("Internal error while updating issue.") + } + + issueResult, err := h.ListIssues(&entity.IssueFilter{Id: []*int64{&issue.Id}}, &entity.ListOptions{}) + + if err != nil { + l.Error(err) + return nil, heurekaError("Internal error while retrieving updated issue.") + } + + if len(issueResult.Elements) != 1 { + l.Error(err) + return nil, heurekaError("Multiple issues found.") + } + + return issueResult.Elements[0].Issue, nil +} + +func (h *HeurekaApp) DeleteIssue(id int64) error { + l := logrus.WithFields(logrus.Fields{ + "event": "app.DeleteIssue", + "id": id, + }) + + err := h.database.DeleteIssue(id) + + if err != nil { + l.Error(err) + return heurekaError("Internal error while deleting issue.") + } + + return nil +} diff --git a/internal/app/issue_test.go b/internal/app/issue_test.go index 1b1dc8df..812ff127 100644 --- a/internal/app/issue_test.go +++ b/internal/app/issue_test.go @@ -179,3 +179,115 @@ var _ = Describe("When listing Issues", Label("app", "ListIssues"), func() { }) }) }) + +var _ = Describe("When creating Issue", Label("app", "CreateIssue"), func() { + var ( + db *mocks.MockDatabase + heureka app.Heureka + issue entity.Issue + filter *entity.IssueFilter + ) + + BeforeEach(func() { + db = mocks.NewMockDatabase(GinkgoT()) + issue = test.NewFakeIssueEntity() + first := 10 + var after int64 + after = 0 + filter = &entity.IssueFilter{ + Paginated: entity.Paginated{ + First: &first, + After: &after, + }, + } + }) + + It("creates issue", func() { + filter.PrimaryName = []*string{&issue.PrimaryName} + db.On("CreateIssue", &issue).Return(&issue, nil) + db.On("GetIssues", filter).Return([]entity.Issue{}, nil) + heureka = app.NewHeurekaApp(db) + newIssue, err := heureka.CreateIssue(&issue) + Expect(err).To(BeNil(), "no error should be thrown") + Expect(newIssue.Id).NotTo(BeEquivalentTo(0)) + By("setting fields", func() { + Expect(newIssue.PrimaryName).To(BeEquivalentTo(issue.PrimaryName)) + Expect(newIssue.Description).To(BeEquivalentTo(issue.Description)) + Expect(newIssue.Type.String()).To(BeEquivalentTo(issue.Type.String())) + }) + }) +}) + +var _ = Describe("When updating Issue", Label("app", "UpdateIssue"), func() { + var ( + db *mocks.MockDatabase + heureka app.Heureka + issue entity.Issue + filter *entity.IssueFilter + ) + + BeforeEach(func() { + db = mocks.NewMockDatabase(GinkgoT()) + issue = test.NewFakeIssueEntity() + first := 10 + var after int64 + after = 0 + filter = &entity.IssueFilter{ + Paginated: entity.Paginated{ + First: &first, + After: &after, + }, + } + }) + + It("updates issue", func() { + db.On("UpdateIssue", &issue).Return(nil) + heureka = app.NewHeurekaApp(db) + issue.Description = "New Description" + filter.Id = []*int64{&issue.Id} + db.On("GetIssues", filter).Return([]entity.Issue{issue}, nil) + updatedIssue, err := heureka.UpdateIssue(&issue) + Expect(err).To(BeNil(), "no error should be thrown") + By("setting fields", func() { + Expect(updatedIssue.PrimaryName).To(BeEquivalentTo(issue.PrimaryName)) + Expect(updatedIssue.Description).To(BeEquivalentTo(issue.Description)) + Expect(updatedIssue.Type.String()).To(BeEquivalentTo(issue.Type.String())) + }) + }) +}) + +var _ = Describe("When deleting Issue", Label("app", "DeleteIssue"), func() { + var ( + db *mocks.MockDatabase + heureka app.Heureka + id int64 + filter *entity.IssueFilter + ) + + BeforeEach(func() { + db = mocks.NewMockDatabase(GinkgoT()) + id = 1 + first := 10 + var after int64 + after = 0 + filter = &entity.IssueFilter{ + Paginated: entity.Paginated{ + First: &first, + After: &after, + }, + } + }) + + It("deletes issue", func() { + db.On("DeleteIssue", id).Return(nil) + heureka = app.NewHeurekaApp(db) + db.On("GetIssues", filter).Return([]entity.Issue{}, nil) + err := heureka.DeleteIssue(id) + Expect(err).To(BeNil(), "no error should be thrown") + + filter.Id = []*int64{&id} + issues, err := heureka.ListIssues(filter, &entity.ListOptions{}) + Expect(err).To(BeNil(), "no error should be thrown") + Expect(issues.Elements).To(BeEmpty(), "no error should be thrown") + }) +}) diff --git a/internal/database/interface.go b/internal/database/interface.go index 92521dda..b7f18690 100644 --- a/internal/database/interface.go +++ b/internal/database/interface.go @@ -10,6 +10,9 @@ type Database interface { GetIssuesWithAggregations(*entity.IssueFilter) ([]entity.IssueWithAggregations, error) CountIssues(*entity.IssueFilter) (int64, error) GetAllIssueIds(*entity.IssueFilter) ([]int64, error) + CreateIssue(*entity.Issue) (*entity.Issue, error) + UpdateIssue(*entity.Issue) error + DeleteIssue(int64) error GetIssueVariants(*entity.IssueVariantFilter) ([]entity.IssueVariant, error) GetAllIssueVariantIds(*entity.IssueVariantFilter) ([]int64, error) diff --git a/internal/database/mariadb/entity.go b/internal/database/mariadb/entity.go index 84c0b8ab..d4810c17 100644 --- a/internal/database/mariadb/entity.go +++ b/internal/database/mariadb/entity.go @@ -134,6 +134,31 @@ func (ibr *GetIssuesByRow) AsIssueWithAggregations() entity.IssueWithAggregation } } +func (ibr *GetIssuesByRow) AsIssue() entity.Issue { + return entity.Issue{ + Id: GetInt64Value(ibr.IssueRow.Id), + PrimaryName: GetStringValue(ibr.IssueRow.PrimaryName), + Type: entity.NewIssueType(GetStringValue(ibr.Type)), + Description: GetStringValue(ibr.IssueRow.Description), + IssueVariants: []entity.IssueVariant{}, + IssueMatches: []entity.IssueMatch{}, + Activity: []entity.Activity{}, + CreatedAt: GetTimeValue(ibr.IssueRow.CreatedAt), + DeletedAt: GetTimeValue(ibr.IssueRow.DeletedAt), + UpdatedAt: GetTimeValue(ibr.IssueRow.UpdatedAt), + } +} + +func (ir *IssueRow) FromIssue(i *entity.Issue) { + ir.Id = sql.NullInt64{Int64: i.Id, Valid: true} + ir.PrimaryName = sql.NullString{String: i.PrimaryName, Valid: true} + ir.Type = sql.NullString{String: i.Type.String(), Valid: true} + ir.Description = sql.NullString{String: i.Description, Valid: true} + ir.CreatedAt = sql.NullTime{Time: i.CreatedAt, Valid: true} + ir.DeletedAt = sql.NullTime{Time: i.DeletedAt, Valid: true} + ir.UpdatedAt = sql.NullTime{Time: i.UpdatedAt, Valid: true} +} + type IssueMatchRow struct { Id sql.NullInt64 `db:"issuematch_id" json:"id"` Status sql.NullString `db:"issuematch_status" json:"status"` diff --git a/internal/database/mariadb/issue.go b/internal/database/mariadb/issue.go index bbb2096f..10a866d5 100644 --- a/internal/database/mariadb/issue.go +++ b/internal/database/mariadb/issue.go @@ -22,6 +22,8 @@ func (s *SqlDatabase) getIssueFilterString(filter *entity.IssueFilter) string { fl = append(fl, buildFilterQuery(filter.ComponentVersionId, "CVI.componentversionissue_component_version_id = ?", OP_OR)) fl = append(fl, buildFilterQuery(filter.IssueVariantId, "IV.issuevariant_id = ?", OP_OR)) 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, "I.issue_deleted_at IS NULL") return combineFilterQueries(fl, OP_AND) } @@ -93,6 +95,20 @@ func (s *SqlDatabase) ensureIssueFilter(f *entity.IssueFilter) *entity.IssueFilt return f } +func (s *SqlDatabase) getIssueUpdateFields(issue *entity.Issue) string { + fl := []string{} + if issue.PrimaryName != "" { + fl = append(fl, "issue_primary_name = :issue_primary_name") + } + if issue.Type != "" { + fl = append(fl, "issue_type = :issue_type") + } + if issue.Description != "" { + fl = append(fl, "issue_description = :issue_description") + } + return strings.Join(fl, ", ") +} + // buildGetIssuesStatement is building the prepared statement and its parameters from the provided filter // // The where clause is build as follows: @@ -146,6 +162,7 @@ func (s *SqlDatabase) buildGetIssuesStatement(filter *entity.IssueFilter, aggreg filterParameters = buildQueryParameters(filterParameters, filter.ComponentVersionId) filterParameters = buildQueryParameters(filterParameters, filter.IssueVariantId) filterParameters = buildQueryParameters(filterParameters, filter.Type) + filterParameters = buildQueryParameters(filterParameters, filter.PrimaryName) filterParameters = append(filterParameters, cursor.Value) filterParameters = append(filterParameters, cursor.Limit) @@ -202,6 +219,7 @@ func (s *SqlDatabase) buildCountIssuesStatement(filter *entity.IssueFilter) (*sq filterParameters = buildQueryParameters(filterParameters, filter.ComponentVersionId) filterParameters = buildQueryParameters(filterParameters, filter.IssueVariantId) filterParameters = buildQueryParameters(filterParameters, filter.Type) + filterParameters = buildQueryParameters(filterParameters, filter.PrimaryName) l.WithFields(logrus.Fields{ "query": query, @@ -291,6 +309,7 @@ func (s *SqlDatabase) GetAllIssueIds(filter *entity.IssueFilter) ([]int64, error filterParameters = buildQueryParameters(filterParameters, filter.ComponentVersionId) filterParameters = buildQueryParameters(filterParameters, filter.IssueVariantId) filterParameters = buildQueryParameters(filterParameters, filter.Type) + filterParameters = buildQueryParameters(filterParameters, filter.PrimaryName) return performIdScan(stmt, filterParameters, l) } @@ -345,3 +364,80 @@ func (s *SqlDatabase) CountIssues(filter *entity.IssueFilter) (int64, error) { return performCountScan(stmt, filterParameters, l) } + +func (s *SqlDatabase) CreateIssue(issue *entity.Issue) (*entity.Issue, error) { + l := logrus.WithFields(logrus.Fields{ + "issue": issue, + "event": "database.CreateIssue", + }) + + query := ` + INSERT INTO Issue ( + issue_primary_name, + issue_type, + issue_description + ) VALUES ( + :issue_primary_name, + :issue_type, + :issue_description + ) + ` + + issueRow := IssueRow{} + issueRow.FromIssue(issue) + + id, err := performInsert(s, query, issueRow, l) + + if err != nil { + return nil, err + } + + issue.Id = id + + return issue, nil +} + +func (s *SqlDatabase) UpdateIssue(issue *entity.Issue) error { + l := logrus.WithFields(logrus.Fields{ + "issue": issue, + "event": "database.UpdateIssue", + }) + + baseQuery := ` + UPDATE Issue SET + %s + WHERE issue_id = :issue_id + ` + + updateFields := s.getIssueUpdateFields(issue) + + query := fmt.Sprintf(baseQuery, updateFields) + + issueRow := IssueRow{} + issueRow.FromIssue(issue) + + _, err := performExec(s, query, issueRow, l) + + return err +} + +func (s *SqlDatabase) DeleteIssue(id int64) error { + l := logrus.WithFields(logrus.Fields{ + "id": id, + "event": "database.DeleteIssue", + }) + + query := ` + UPDATE Issue SET + issue_deleted_at = NOW() + WHERE issue_id = :id + ` + + args := map[string]interface{}{ + "id": id, + } + + _, err := performExec(s, query, args, l) + + return err +} diff --git a/internal/database/mariadb/issue_test.go b/internal/database/mariadb/issue_test.go index fc53af53..b66b608c 100644 --- a/internal/database/mariadb/issue_test.go +++ b/internal/database/mariadb/issue_test.go @@ -451,4 +451,118 @@ var _ = Describe("Issue", Label("database", "Issue"), func() { }) }) }) + When("Insert Issue", Label("InsertIssue"), func() { + Context("and we have 10 Issues in the database", func() { + var newIssueRow mariadb.IssueRow + var newIssue entity.Issue + var seedCollection *test.SeedCollection + BeforeEach(func() { + seedCollection = seeder.SeedDbWithNFakeData(10) + newIssueRow = test.NewFakeIssue() + newIssue = newIssueRow.AsIssue() + }) + It("can insert correctly", func() { + issue, err := db.CreateIssue(&newIssue) + + By("throwing no error", func() { + Expect(err).To(BeNil()) + }) + By("sets issue id", func() { + Expect(issue).NotTo(BeEquivalentTo(0)) + }) + + issueFilter := &entity.IssueFilter{ + Id: []*int64{&issue.Id}, + } + + i, err := db.GetIssues(issueFilter) + By("throwing no error", func() { + Expect(err).To(BeNil()) + }) + By("returning issue", func() { + Expect(len(i)).To(BeEquivalentTo(1)) + }) + By("setting fields", func() { + Expect(i[0].PrimaryName).To(BeEquivalentTo(issue.PrimaryName)) + Expect(i[0].Type.String()).To(BeEquivalentTo(issue.Type.String())) + Expect(i[0].Description).To(BeEquivalentTo(issue.Description)) + }) + }) + It("does not insert issue with existing primary name", func() { + issueRow := seedCollection.IssueRows[0] + issue := issueRow.AsIssue() + newIssue, err := db.CreateIssue(&issue) + + By("throwing error", func() { + Expect(err).ToNot(BeNil()) + }) + By("no issue returned", func() { + Expect(newIssue).To(BeNil()) + }) + + }) + }) + }) + When("Update Issue", Label("UpdateIssue"), func() { + Context("and we have 10 Issues in the database", func() { + var seedCollection *test.SeedCollection + BeforeEach(func() { + seedCollection = seeder.SeedDbWithNFakeData(10) + }) + It("can update issue description correctly", func() { + issue := seedCollection.IssueRows[0].AsIssue() + + issue.Description = "New Description" + err := db.UpdateIssue(&issue) + + By("throwing no error", func() { + Expect(err).To(BeNil()) + }) + + issueFilter := &entity.IssueFilter{ + Id: []*int64{&issue.Id}, + } + + i, err := db.GetIssues(issueFilter) + By("throwing no error", func() { + Expect(err).To(BeNil()) + }) + By("returning issue", func() { + Expect(len(i)).To(BeEquivalentTo(1)) + }) + By("setting fields", func() { + Expect(i[0].Description).To(BeEquivalentTo(issue.Description)) + }) + }) + }) + }) + When("Delete Issue", Label("DeleteIssue"), func() { + Context("and we have 10 Issues in the database", func() { + var seedCollection *test.SeedCollection + BeforeEach(func() { + seedCollection = seeder.SeedDbWithNFakeData(10) + }) + It("can delete issue correctly", func() { + issue := seedCollection.IssueRows[0].AsIssue() + + err := db.DeleteIssue(issue.Id) + + By("throwing no error", func() { + Expect(err).To(BeNil()) + }) + + issueFilter := &entity.IssueFilter{ + Id: []*int64{&issue.Id}, + } + + i, err := db.GetIssues(issueFilter) + By("throwing no error", func() { + Expect(err).To(BeNil()) + }) + By("returning no issue", func() { + Expect(len(i)).To(BeEquivalentTo(0)) + }) + }) + }) + }) }) diff --git a/internal/e2e/issue_query_test.go b/internal/e2e/issue_query_test.go index e21fb912..a1138be6 100644 --- a/internal/e2e/issue_query_test.go +++ b/internal/e2e/issue_query_test.go @@ -8,6 +8,8 @@ import ( "fmt" "os" + "github.wdf.sap.corp/cc/heureka/internal/entity" + testentity "github.wdf.sap.corp/cc/heureka/internal/entity/test" "github.wdf.sap.corp/cc/heureka/internal/util" util2 "github.wdf.sap.corp/cc/heureka/pkg/util" @@ -200,3 +202,198 @@ var _ = Describe("Getting Issues via API", Label("e2e", "Issues"), func() { }) }) }) + +var _ = Describe("Creating Issue via API", Label("e2e", "Issues"), func() { + + var seeder *test.DatabaseSeeder + var s *server.Server + var cfg util.Config + var issue entity.Issue + + BeforeEach(func() { + var err error + _ = dbm.NewTestSchema() + seeder, err = test.NewDatabaseSeeder(dbm.DbConfig()) + Expect(err).To(BeNil(), "Database Seeder Setup should work") + + cfg = dbm.DbConfig() + cfg.Port = util2.GetRandomFreePort() + s = server.NewServer(cfg) + + s.NonBlockingStart() + }) + + AfterEach(func() { + s.BlockingStop() + }) + + When("the database has 10 entries", func() { + + BeforeEach(func() { + seeder.SeedDbWithNFakeData(10) + issue = testentity.NewFakeIssueEntity() + }) + + Context("and a mutation query is performed", Label("create.graphql"), func() { + It("creates new issue", func() { + // create a queryCollection (safe to share across requests) + client := graphql.NewClient(fmt.Sprintf("http://localhost:%s/query", cfg.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/create.graphql") + + Expect(err).To(BeNil()) + str := string(b) + req := graphql.NewRequest(str) + + req.Var("input", map[string]string{ + "primaryName": issue.PrimaryName, + "description": issue.Description, + "type": issue.Type.String(), + }) + + req.Header.Set("Cache-Control", "no-cache") + ctx := context.Background() + + var respData struct { + Issue model.Issue `json:"createIssue"` + } + if err := util2.RequestWithBackoff(func() error { return client.Run(ctx, req, &respData) }); err != nil { + logrus.WithError(err).WithField("request", req).Fatalln("Error while unmarshaling") + } + + Expect(*respData.Issue.PrimaryName).To(Equal(issue.PrimaryName)) + Expect(*respData.Issue.Description).To(Equal(issue.Description)) + Expect(respData.Issue.Type.String()).To(Equal(issue.Type.String())) + }) + }) + }) +}) + +var _ = Describe("Updating issue via API", Label("e2e", "Issues"), func() { + + var seeder *test.DatabaseSeeder + var s *server.Server + var cfg util.Config + + BeforeEach(func() { + var err error + _ = dbm.NewTestSchema() + seeder, err = test.NewDatabaseSeeder(dbm.DbConfig()) + Expect(err).To(BeNil(), "Database Seeder Setup should work") + + cfg = dbm.DbConfig() + cfg.Port = util2.GetRandomFreePort() + s = server.NewServer(cfg) + + s.NonBlockingStart() + }) + + AfterEach(func() { + s.BlockingStop() + }) + + When("the database has 10 entries", func() { + var seedCollection *test.SeedCollection + + BeforeEach(func() { + seedCollection = seeder.SeedDbWithNFakeData(10) + }) + + Context("and a mutation query is performed", Label("update.graphql"), func() { + It("updates issue", func() { + // create a queryCollection (safe to share across requests) + client := graphql.NewClient(fmt.Sprintf("http://localhost:%s/query", cfg.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/update.graphql") + + Expect(err).To(BeNil()) + str := string(b) + req := graphql.NewRequest(str) + + issue := seedCollection.IssueRows[0].AsIssue() + issue.Description = "New Description" + + req.Var("id", fmt.Sprintf("%d", issue.Id)) + req.Var("input", map[string]string{ + "description": issue.Description, + }) + + req.Header.Set("Cache-Control", "no-cache") + ctx := context.Background() + + var respData struct { + Issue model.Issue `json:"updateIssue"` + } + if err := util2.RequestWithBackoff(func() error { return client.Run(ctx, req, &respData) }); err != nil { + logrus.WithError(err).WithField("request", req).Fatalln("Error while unmarshaling") + } + + Expect(*respData.Issue.Description).To(Equal(issue.Description)) + }) + }) + }) +}) + +var _ = Describe("Deleting Issue via API", Label("e2e", "Issues"), func() { + + var seeder *test.DatabaseSeeder + var s *server.Server + var cfg util.Config + + BeforeEach(func() { + var err error + _ = dbm.NewTestSchema() + seeder, err = test.NewDatabaseSeeder(dbm.DbConfig()) + Expect(err).To(BeNil(), "Database Seeder Setup should work") + + cfg = dbm.DbConfig() + cfg.Port = util2.GetRandomFreePort() + s = server.NewServer(cfg) + + s.NonBlockingStart() + }) + + AfterEach(func() { + s.BlockingStop() + }) + + When("the database has 10 entries", func() { + var seedCollection *test.SeedCollection + + BeforeEach(func() { + seedCollection = seeder.SeedDbWithNFakeData(10) + }) + + Context("and a mutation query is performed", Label("delete.graphql"), func() { + It("deletes issue", func() { + // create a queryCollection (safe to share across requests) + client := graphql.NewClient(fmt.Sprintf("http://localhost:%s/query", cfg.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) + + id := fmt.Sprintf("%d", seedCollection.ServiceRows[0].Id.Int64) + + req.Var("id", id) + + 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") + } + + Expect(respData.Id).To(Equal(id)) + }) + }) + }) +}) diff --git a/internal/entity/test/issue.go b/internal/entity/test/issue.go index a509a472..44c96748 100644 --- a/internal/entity/test/issue.go +++ b/internal/entity/test/issue.go @@ -4,15 +4,20 @@ package test import ( + "fmt" + "github.com/brianvoe/gofakeit/v7" "github.wdf.sap.corp/cc/heureka/internal/entity" ) func NewFakeIssueEntity() entity.Issue { + t := gofakeit.RandomString(entity.AllIssueTypes) + primaryName := fmt.Sprintf("CVE-%d-%d", gofakeit.Year(), gofakeit.Number(100, 9999999)) return entity.Issue{ Id: int64(gofakeit.Number(1, 10000000)), - PrimaryName: gofakeit.Name(), + PrimaryName: primaryName, Description: gofakeit.AdjectiveDescriptive(), + Type: entity.NewIssueType(t), IssueVariants: nil, IssueMatches: nil, ComponentVersions: nil, diff --git a/internal/mocks/mock_Database.go b/internal/mocks/mock_Database.go index bb15850a..84a94451 100644 --- a/internal/mocks/mock_Database.go +++ b/internal/mocks/mock_Database.go @@ -1,6 +1,3 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors -// SPDX-License-Identifier: Apache-2.0 - // Code generated by mockery v2.42.1. DO NOT EDIT. package mocks @@ -1086,6 +1083,64 @@ func (_c *MockDatabase_CreateEvidence_Call) RunAndReturn(run func(*entity.Eviden return _c } +// CreateIssue provides a mock function with given fields: _a0 +func (_m *MockDatabase) CreateIssue(_a0 *entity.Issue) (*entity.Issue, error) { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for CreateIssue") + } + + var r0 *entity.Issue + var r1 error + if rf, ok := ret.Get(0).(func(*entity.Issue) (*entity.Issue, error)); ok { + return rf(_a0) + } + if rf, ok := ret.Get(0).(func(*entity.Issue) *entity.Issue); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*entity.Issue) + } + } + + if rf, ok := ret.Get(1).(func(*entity.Issue) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockDatabase_CreateIssue_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateIssue' +type MockDatabase_CreateIssue_Call struct { + *mock.Call +} + +// CreateIssue is a helper method to define mock.On call +// - _a0 *entity.Issue +func (_e *MockDatabase_Expecter) CreateIssue(_a0 interface{}) *MockDatabase_CreateIssue_Call { + return &MockDatabase_CreateIssue_Call{Call: _e.mock.On("CreateIssue", _a0)} +} + +func (_c *MockDatabase_CreateIssue_Call) Run(run func(_a0 *entity.Issue)) *MockDatabase_CreateIssue_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(*entity.Issue)) + }) + return _c +} + +func (_c *MockDatabase_CreateIssue_Call) Return(_a0 *entity.Issue, _a1 error) *MockDatabase_CreateIssue_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockDatabase_CreateIssue_Call) RunAndReturn(run func(*entity.Issue) (*entity.Issue, error)) *MockDatabase_CreateIssue_Call { + _c.Call.Return(run) + return _c +} + // CreateIssueMatch provides a mock function with given fields: _a0 func (_m *MockDatabase) CreateIssueMatch(_a0 *entity.IssueMatch) (*entity.IssueMatch, error) { ret := _m.Called(_a0) @@ -1664,6 +1719,52 @@ func (_c *MockDatabase_DeleteEvidence_Call) RunAndReturn(run func(int64) error) return _c } +// DeleteIssue provides a mock function with given fields: _a0 +func (_m *MockDatabase) DeleteIssue(_a0 int64) error { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for DeleteIssue") + } + + var r0 error + if rf, ok := ret.Get(0).(func(int64) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockDatabase_DeleteIssue_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteIssue' +type MockDatabase_DeleteIssue_Call struct { + *mock.Call +} + +// DeleteIssue is a helper method to define mock.On call +// - _a0 int64 +func (_e *MockDatabase_Expecter) DeleteIssue(_a0 interface{}) *MockDatabase_DeleteIssue_Call { + return &MockDatabase_DeleteIssue_Call{Call: _e.mock.On("DeleteIssue", _a0)} +} + +func (_c *MockDatabase_DeleteIssue_Call) Run(run func(_a0 int64)) *MockDatabase_DeleteIssue_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(int64)) + }) + return _c +} + +func (_c *MockDatabase_DeleteIssue_Call) Return(_a0 error) *MockDatabase_DeleteIssue_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockDatabase_DeleteIssue_Call) RunAndReturn(run func(int64) error) *MockDatabase_DeleteIssue_Call { + _c.Call.Return(run) + return _c +} + // DeleteIssueMatch provides a mock function with given fields: _a0 func (_m *MockDatabase) DeleteIssueMatch(_a0 int64) error { ret := _m.Called(_a0) @@ -3736,6 +3837,52 @@ func (_c *MockDatabase_UpdateEvidence_Call) RunAndReturn(run func(*entity.Eviden return _c } +// UpdateIssue provides a mock function with given fields: _a0 +func (_m *MockDatabase) UpdateIssue(_a0 *entity.Issue) error { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for UpdateIssue") + } + + var r0 error + if rf, ok := ret.Get(0).(func(*entity.Issue) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockDatabase_UpdateIssue_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateIssue' +type MockDatabase_UpdateIssue_Call struct { + *mock.Call +} + +// UpdateIssue is a helper method to define mock.On call +// - _a0 *entity.Issue +func (_e *MockDatabase_Expecter) UpdateIssue(_a0 interface{}) *MockDatabase_UpdateIssue_Call { + return &MockDatabase_UpdateIssue_Call{Call: _e.mock.On("UpdateIssue", _a0)} +} + +func (_c *MockDatabase_UpdateIssue_Call) Run(run func(_a0 *entity.Issue)) *MockDatabase_UpdateIssue_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(*entity.Issue)) + }) + return _c +} + +func (_c *MockDatabase_UpdateIssue_Call) Return(_a0 error) *MockDatabase_UpdateIssue_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockDatabase_UpdateIssue_Call) RunAndReturn(run func(*entity.Issue) error) *MockDatabase_UpdateIssue_Call { + _c.Call.Return(run) + return _c +} + // UpdateIssueMatch provides a mock function with given fields: _a0 func (_m *MockDatabase) UpdateIssueMatch(_a0 *entity.IssueMatch) error { ret := _m.Called(_a0) diff --git a/internal/mocks/mock_Heureka.go b/internal/mocks/mock_Heureka.go index 36064945..bb9f1386 100644 --- a/internal/mocks/mock_Heureka.go +++ b/internal/mocks/mock_Heureka.go @@ -1,6 +1,3 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors -// SPDX-License-Identifier: Apache-2.0 - // Code generated by mockery v2.42.1. DO NOT EDIT. package mocks @@ -23,6 +20,64 @@ func (_m *MockHeureka) EXPECT() *MockHeureka_Expecter { return &MockHeureka_Expecter{mock: &_m.Mock} } +// CreateActivity provides a mock function with given fields: _a0 +func (_m *MockHeureka) CreateActivity(_a0 *entity.Activity) (*entity.Activity, error) { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for CreateActivity") + } + + var r0 *entity.Activity + var r1 error + if rf, ok := ret.Get(0).(func(*entity.Activity) (*entity.Activity, error)); ok { + return rf(_a0) + } + if rf, ok := ret.Get(0).(func(*entity.Activity) *entity.Activity); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*entity.Activity) + } + } + + if rf, ok := ret.Get(1).(func(*entity.Activity) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockHeureka_CreateActivity_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateActivity' +type MockHeureka_CreateActivity_Call struct { + *mock.Call +} + +// CreateActivity is a helper method to define mock.On call +// - _a0 *entity.Activity +func (_e *MockHeureka_Expecter) CreateActivity(_a0 interface{}) *MockHeureka_CreateActivity_Call { + return &MockHeureka_CreateActivity_Call{Call: _e.mock.On("CreateActivity", _a0)} +} + +func (_c *MockHeureka_CreateActivity_Call) Run(run func(_a0 *entity.Activity)) *MockHeureka_CreateActivity_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(*entity.Activity)) + }) + return _c +} + +func (_c *MockHeureka_CreateActivity_Call) Return(_a0 *entity.Activity, _a1 error) *MockHeureka_CreateActivity_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockHeureka_CreateActivity_Call) RunAndReturn(run func(*entity.Activity) (*entity.Activity, error)) *MockHeureka_CreateActivity_Call { + _c.Call.Return(run) + return _c +} + // CreateComponent provides a mock function with given fields: _a0 func (_m *MockHeureka) CreateComponent(_a0 *entity.Component) (*entity.Component, error) { ret := _m.Called(_a0) @@ -603,6 +658,52 @@ func (_c *MockHeureka_CreateUser_Call) RunAndReturn(run func(*entity.User) (*ent return _c } +// DeleteActivity provides a mock function with given fields: _a0 +func (_m *MockHeureka) DeleteActivity(_a0 int64) error { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for DeleteActivity") + } + + var r0 error + if rf, ok := ret.Get(0).(func(int64) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockHeureka_DeleteActivity_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteActivity' +type MockHeureka_DeleteActivity_Call struct { + *mock.Call +} + +// DeleteActivity is a helper method to define mock.On call +// - _a0 int64 +func (_e *MockHeureka_Expecter) DeleteActivity(_a0 interface{}) *MockHeureka_DeleteActivity_Call { + return &MockHeureka_DeleteActivity_Call{Call: _e.mock.On("DeleteActivity", _a0)} +} + +func (_c *MockHeureka_DeleteActivity_Call) Run(run func(_a0 int64)) *MockHeureka_DeleteActivity_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(int64)) + }) + return _c +} + +func (_c *MockHeureka_DeleteActivity_Call) Return(_a0 error) *MockHeureka_DeleteActivity_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockHeureka_DeleteActivity_Call) RunAndReturn(run func(int64) error) *MockHeureka_DeleteActivity_Call { + _c.Call.Return(run) + return _c +} + // DeleteComponent provides a mock function with given fields: _a0 func (_m *MockHeureka) DeleteComponent(_a0 int64) error { ret := _m.Called(_a0) @@ -1992,6 +2093,64 @@ func (_c *MockHeureka_Shutdown_Call) RunAndReturn(run func() error) *MockHeureka return _c } +// UpdateActivity provides a mock function with given fields: _a0 +func (_m *MockHeureka) UpdateActivity(_a0 *entity.Activity) (*entity.Activity, error) { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for UpdateActivity") + } + + var r0 *entity.Activity + var r1 error + if rf, ok := ret.Get(0).(func(*entity.Activity) (*entity.Activity, error)); ok { + return rf(_a0) + } + if rf, ok := ret.Get(0).(func(*entity.Activity) *entity.Activity); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*entity.Activity) + } + } + + if rf, ok := ret.Get(1).(func(*entity.Activity) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockHeureka_UpdateActivity_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateActivity' +type MockHeureka_UpdateActivity_Call struct { + *mock.Call +} + +// UpdateActivity is a helper method to define mock.On call +// - _a0 *entity.Activity +func (_e *MockHeureka_Expecter) UpdateActivity(_a0 interface{}) *MockHeureka_UpdateActivity_Call { + return &MockHeureka_UpdateActivity_Call{Call: _e.mock.On("UpdateActivity", _a0)} +} + +func (_c *MockHeureka_UpdateActivity_Call) Run(run func(_a0 *entity.Activity)) *MockHeureka_UpdateActivity_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(*entity.Activity)) + }) + return _c +} + +func (_c *MockHeureka_UpdateActivity_Call) Return(_a0 *entity.Activity, _a1 error) *MockHeureka_UpdateActivity_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockHeureka_UpdateActivity_Call) RunAndReturn(run func(*entity.Activity) (*entity.Activity, error)) *MockHeureka_UpdateActivity_Call { + _c.Call.Return(run) + return _c +} + // UpdateComponent provides a mock function with given fields: _a0 func (_m *MockHeureka) UpdateComponent(_a0 *entity.Component) (*entity.Component, error) { ret := _m.Called(_a0)