Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Weos 1271 ( Included 1178 and 1131 code (attach delete endpoint and actual functionality) #73

Merged
merged 12 commits into from
Feb 2, 2022
Merged
4 changes: 4 additions & 0 deletions api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,10 @@ paths:
type: string
required: true
description: blog id
- in: header
name: If-Match
schema:
type: string
requestBody:
description: Blog info that is submitted
required: false
Expand Down
115 changes: 115 additions & 0 deletions controllers/rest/controller_standard.go
Original file line number Diff line number Diff line change
Expand Up @@ -537,7 +537,122 @@ func (c *StandardControllers) List(app model.Service, spec *openapi3.Swagger, pa
}

func (c *StandardControllers) Delete(app model.Service, spec *openapi3.Swagger, path *openapi3.PathItem, operation *openapi3.Operation) echo.HandlerFunc {
var contentType string
var contentTypeSchema *openapi3.SchemaRef
//get the entity information based on the Content Type associated with this operation
for _, requestContent := range operation.RequestBody.Value.Content {
//use the first schema ref to determine the entity type
if requestContent.Schema.Ref != "" {
contentType = strings.Replace(requestContent.Schema.Ref, "#/components/schemas/", "", -1)
//get the schema details from the swagger file
contentTypeSchema = spec.Components.Schemas[contentType]
break
}
}
return func(ctxt echo.Context) error {
//look up the schema for the content type so that we could identify the rules
newContext := ctxt.Request().Context()
cType := &context2.ContentType{}
if contentType != "" && contentTypeSchema.Value != nil {
cType = &context2.ContentType{
Name: contentType,
Schema: contentTypeSchema.Value,
}
newContext = context.WithValue(newContext, context2.CONTENT_TYPE, cType)
}
var weosID string
var sequenceNo string
var Etag string

//getting etag from context
etagInterface := newContext.Value("If-Match")
if etagInterface != nil {
if etag, ok := etagInterface.(string); ok {
if etag != "" {
Etag = etag
weosID, sequenceNo = SplitEtag(etag)
seq, err := strconv.Atoi(sequenceNo)
if err != nil {
return NewControllerError("unexpected error deleting content type. invalid sequence number", err, http.StatusBadRequest)
}
newContext = context.WithValue(newContext, context2.WEOS_ID, weosID)
newContext = context.WithValue(newContext, context2.SEQUENCE_NO, seq)
}
}
}

var err error
var identifiers []string
var result1 map[string]interface{}
var weos_id string
var ok bool

//Uses the identifiers to pull the weosID, to be later used to get Seq NO
if etagInterface == nil {
//find entity based on identifiers specified
pks, _ := json.Marshal(contentTypeSchema.Value.Extensions["x-identifier"])
json.Unmarshal(pks, &identifiers)

if len(identifiers) == 0 {
identifiers = append(identifiers, "id")
}

primaryKeys := map[string]interface{}{}
for _, p := range identifiers {

ctxtIdentifier := newContext.Value(p)

primaryKeys[p] = ctxtIdentifier

}

for _, projection := range app.Projections() {
if projection != nil {
result1, err = projection.GetByKey(newContext, *cType, primaryKeys)
if err != nil {
return err
}

}
}
weos_id, ok = result1["weos_id"].(string)

if (len(result1) == 0) || !ok || weos_id == "" {
return NewControllerError("No entity found", err, http.StatusNotFound)
} else if err != nil {
return NewControllerError(err.Error(), err, http.StatusBadRequest)
}
}

//Dispatch the actual delete to projecitons
err = app.Dispatcher().Dispatch(newContext, model.Delete(newContext, contentType, weosID))
if err != nil {
if errr, ok := err.(*model.DomainError); ok {
if strings.Contains(errr.Error(), "error deleting entity. This is a stale item") {
return NewControllerError(errr.Error(), err, http.StatusPreconditionFailed)
}
if strings.Contains(errr.Error(), "invalid:") {
return NewControllerError(errr.Error(), err, http.StatusUnprocessableEntity)
}
return NewControllerError(errr.Error(), err, http.StatusBadRequest)
} else {
return NewControllerError("unexpected error deleting content type", err, http.StatusBadRequest)
}
}

deleteEventSeq, err := app.EventRepository().GetAggregateSequenceNumber(weos_id)
if err != nil {
return NewControllerError("No delete event found", err, http.StatusNotFound)
}

Etag = NewEtag(&model.ContentEntity{
AggregateRoot: model.AggregateRoot{
SequenceNo: deleteEventSeq,
BasicEntity: model.BasicEntity{ID: weos_id},
},
})

ctxt.Response().Header().Set("Etag", Etag)

return ctxt.JSON(http.StatusOK, "Deleted")
}
Expand Down
261 changes: 261 additions & 0 deletions controllers/rest/controller_standard_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1331,3 +1331,264 @@ func TestStandardControllers_FormData_Create(t *testing.T) {
}
})
}

func TestStandardControllers_DeleteEtag(t *testing.T) {
weosId := "123"
mockBlog := &Blog{
Title: "Test Blog",
Description: "testing description",
}

content, err := ioutil.ReadFile("./fixtures/blog.yaml")
if err != nil {
t.Fatalf("error loading api specification '%s'", err)
}
//change the $ref to another marker so that it doesn't get considered an environment variable WECON-1
tempFile := strings.ReplaceAll(string(content), "$ref", "__ref__")
//replace environment variables in file
tempFile = os.ExpandEnv(string(tempFile))
tempFile = strings.ReplaceAll(string(tempFile), "__ref__", "$ref")
//update path so that the open api way of specifying url parameters is change to the echo style of url parameters
re := regexp.MustCompile(`\{([a-zA-Z0-9\-_]+?)\}`)
tempFile = re.ReplaceAllString(tempFile, `:$1`)
content = []byte(tempFile)
loader := openapi3.NewSwaggerLoader()
swagger, err := loader.LoadSwaggerFromData(content)
if err != nil {
t.Fatalf("error loading api specification '%s'", err)
}
//instantiate api
e := echo.New()
restAPI := &rest.RESTAPI{}

dispatcher := &DispatcherMock{
DispatchFunc: func(ctx context.Context, command *model.Command) error {

if command == nil {
t.Fatal("no command sent")
}

if command.Type != "delete" {
t.Errorf("expected the command to be '%s', got '%s'", "delete", command.Type)
}

if command.Metadata.EntityType != "Blog" {
t.Errorf("expected the entity type to be '%s', got '%s'", "Blog", command.Metadata.EntityType)
}

blog := &Blog{}
json.Unmarshal(command.Payload, &blog)

if ctx.Value(weoscontext.WEOS_ID).(string) != weosId {
t.Errorf("expected the blog weos id to be '%s', got '%s'", weosId, ctx.Value(weoscontext.WEOS_ID).(string))
}

//check that content type information is in the context
contentType := weoscontext.GetContentType(ctx)
if contentType == nil {
t.Fatal("expected a content type to be in the context")
}

if contentType.Name != "Blog" {
t.Errorf("expected the content type to be'%s', got %s", "Blog", contentType.Name)
}

if _, ok := contentType.Schema.Properties["title"]; !ok {
t.Errorf("expected a property '%s' on content type '%s'", "title", "blog")
}

if _, ok := contentType.Schema.Properties["description"]; !ok {
t.Errorf("expected a property '%s' on content type '%s'", "description", "blog")
}

id := ctx.Value("id").(string)
if id != weosId {
t.Errorf("unexpected error, expected id to be %s got %s", weosId, id)
}

etag := ctx.Value("If-Match").(string)
if etag != "123.1" {
t.Errorf("unexpected error, expected etag to be %s got %s", "123.1", etag)
}

return nil
},
}
mockEntity := &model.ContentEntity{}
mockEntity.ID = weosId
mockEntity.SequenceNo = int64(1)
mockEntity.Property = mockBlog

projection := &ProjectionMock{
GetByKeyFunc: func(ctxt context.Context, contentType weoscontext.ContentType, identifiers map[string]interface{}) (map[string]interface{}, error) {
return nil, nil
},
GetByEntityIDFunc: func(ctxt context.Context, contentType weoscontext.ContentType, id string) (map[string]interface{}, error) {
return nil, nil
},
GetContentEntityFunc: func(ctx context.Context, weosID string) (*model.ContentEntity, error) {
return mockEntity, nil
},
}

eventMock := &EventRepositoryMock{
GetAggregateSequenceNumberFunc: func(ID string) (int64, error) {
return 2, nil
},
}

application := &ApplicationMock{
DispatcherFunc: func() model.Dispatcher {
return dispatcher
},
ProjectionsFunc: func() []model.Projection {
return []model.Projection{projection}
},
EventRepositoryFunc: func() model.EventRepository {
return eventMock
},
}

//initialization will instantiate with application so we need to overwrite with our mock application
restAPI.Application = application

t.Run("basic delete based on simple content type with id parameter in path and etag", func(t *testing.T) {
paramName := "id"

accountID := "Delete Blog"
path := swagger.Paths.Find("/blogs/:" + paramName)
controller := restAPI.Delete(restAPI.Application, swagger, path, path.Delete)
resp := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodDelete, "/blogs/"+weosId, nil)
req.Header.Set(weoscontext.HeaderXAccountID, accountID)
req.Header.Set("If-Match", weosId+".1")
mw := rest.Context(restAPI.Application, swagger, path, path.Delete)
e.DELETE("/blogs/:"+paramName, controller, mw)
e.ServeHTTP(resp, req)

response := resp.Result()
defer response.Body.Close()

if response.StatusCode != 200 {
t.Errorf("expected response code to be %d, got %d", 200, response.StatusCode)
}
})
}

func TestStandardControllers_DeleteID(t *testing.T) {
mockBlog := &Blog{
Title: "Test Blog",
Description: "testing description",
}

content, err := ioutil.ReadFile("./fixtures/blog.yaml")
if err != nil {
t.Fatalf("error loading api specification '%s'", err)
}
//change the $ref to another marker so that it doesn't get considered an environment variable WECON-1
tempFile := strings.ReplaceAll(string(content), "$ref", "__ref__")
//replace environment variables in file
tempFile = os.ExpandEnv(string(tempFile))
tempFile = strings.ReplaceAll(string(tempFile), "__ref__", "$ref")
//update path so that the open api way of specifying url parameters is change to the echo style of url parameters
re := regexp.MustCompile(`\{([a-zA-Z0-9\-_]+?)\}`)
tempFile = re.ReplaceAllString(tempFile, `:$1`)
content = []byte(tempFile)
loader := openapi3.NewSwaggerLoader()
swagger, err := loader.LoadSwaggerFromData(content)
if err != nil {
t.Fatalf("error loading api specification '%s'", err)
}
//instantiate api
e := echo.New()
restAPI := &rest.RESTAPI{}

dispatcher := &DispatcherMock{
DispatchFunc: func(ctx context.Context, command *model.Command) error {

if command == nil {
t.Fatal("no command sent")
}

if command.Type != "delete" {
t.Errorf("expected the command to be '%s', got '%s'", "delete", command.Type)
}

if command.Metadata.EntityType != "Blog" {
t.Errorf("expected the entity type to be '%s', got '%s'", "Blog", command.Metadata.EntityType)
}

//check that content type information is in the context
contentType := weoscontext.GetContentType(ctx)
if contentType == nil {
t.Fatal("expected a content type to be in the context")
}

if contentType.Name != "Blog" {
t.Errorf("expected the content type to be'%s', got %s", "Blog", contentType.Name)
}

if _, ok := contentType.Schema.Properties["title"]; !ok {
t.Errorf("expected a property '%s' on content type '%s'", "title", "blog")
}

if _, ok := contentType.Schema.Properties["description"]; !ok {
t.Errorf("expected a property '%s' on content type '%s'", "description", "blog")
}

id := ctx.Value("id").(string)
if id != "12" {
t.Errorf("unexpected error, expected id to be %s got %s", "12", id)
}

return nil
},
}
mockEntity := &model.ContentEntity{}
mockEntity.Property = mockBlog

projection := &ProjectionMock{
GetByKeyFunc: func(ctxt context.Context, contentType weoscontext.ContentType, identifiers map[string]interface{}) (map[string]interface{}, error) {
return nil, nil
},
GetByEntityIDFunc: func(ctxt context.Context, contentType weoscontext.ContentType, id string) (map[string]interface{}, error) {
return nil, nil
},
GetContentEntityFunc: func(ctx context.Context, weosID string) (*model.ContentEntity, error) {
return mockEntity, nil
},
}

application := &ApplicationMock{
DispatcherFunc: func() model.Dispatcher {
return dispatcher
},
ProjectionsFunc: func() []model.Projection {
return []model.Projection{projection}
},
}

//initialization will instantiate with application so we need to overwrite with our mock application
restAPI.Application = application

t.Run("basic delete based on simple content type with id parameter in path", func(t *testing.T) {
paramName := "id"

accountID := "Delete Blog"
path := swagger.Paths.Find("/blogs/:" + paramName)
controller := restAPI.Delete(restAPI.Application, swagger, path, path.Delete)
resp := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodDelete, "/blogs/12", nil)
req.Header.Set(weoscontext.HeaderXAccountID, accountID)
mw := rest.Context(restAPI.Application, swagger, path, path.Delete)
e.DELETE("/blogs/:"+paramName, controller, mw)
e.ServeHTTP(resp, req)

response := resp.Result()
defer response.Body.Close()

if response.StatusCode != 200 {
t.Errorf("expected response code to be %d, got %d", 200, response.StatusCode)
}
})
}
Loading