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

Feature/weos 1135 - As a user I should be able to see the details of a specific item #68

Merged
merged 10 commits into from
Jan 29, 2022
2 changes: 1 addition & 1 deletion context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ const ACCOUNT_ID ContextKey = "ACCOUNT_ID"
const USER_ID ContextKey = "USER_ID"
const LOG_LEVEL ContextKey = "LOG_LEVEL"
const REQUEST_ID ContextKey = "REQUEST_ID"
const SEQUENCE_NO ContextKey = "SEQUENCE_NO"
const WEOS_ID ContextKey = "WEOS_ID"
const CONTENT_TYPE ContextKey = "_contentType"
const FILTERS ContextKey = "_filters"
const SORTS ContextKey = "_sorts"
const SEQUENCE_NO string = "sequence_no"

//ContentType this makes it easier to access the content type information in the context
type ContentType struct {
Expand Down
93 changes: 55 additions & 38 deletions controllers/rest/controller_standard.go
Original file line number Diff line number Diff line change
Expand Up @@ -360,70 +360,87 @@ func (c *StandardControllers) View(app model.Service, spec *openapi3.Swagger, pa
identifiers[p] = newContext.Value(p)
}

sequence, _ := newContext.Value("sequence_no").(int)
etag, _ := newContext.Value("If-None-Match").(string)
entityID, _ := newContext.Value("use_entity_id").(bool)

result := map[string]interface{}{}
var result map[string]interface{}
var err error
var entityID string
var seq string
var ok bool
var seqInt int

etag, _ := newContext.Value("If-None-Match").(string)
useEntity, _ := newContext.Value("use_entity_id").(bool)
seqInt, ok = newContext.Value("sequence_no").(int)
if !ok {
ctxt.Logger().Debug("sequence no. not set")
}

//get by keys
if sequence == 0 && etag == "" && !entityID {
//if use_entity_id is not set then let's get the item by key
if !useEntity {
for _, projection := range app.Projections() {
if projection != nil {
result, err = projection.GetByKey(ctxt.Request().Context(), *cType, identifiers)
break
}
}
} else {
id := ""
}
//if etag is set then let's use that info
if etag != "" {
entityID, seq = SplitEtag(etag)
seqInt, err = strconv.Atoi(seq)
if err != nil {
return NewControllerError("Invalid sequence number", err, http.StatusBadRequest)
}
}
//if sequence no. was sent in the request but we don't have the entity let's get it from projection
if entityID == "" && seqInt != 0 {
entityID, ok = result["weos_id"].(string)
if !ok {
ctxt.Logger().Debugf("the item '%v' does not have an entity id stored", identifiers)
}
}

//if etag given, get entity id and sequence number
if etag != "" {
tag, seq := SplitEtag(etag)
id = tag
sequence, err = strconv.Atoi(seq)
if err != nil {
return NewControllerError("Invalid sequence number", err, http.StatusBadRequest)
if useEntity && entityID == "" {
//get first identifier for the entity id
for _, i := range identifiers {
entityID = i.(string)
if entityID != "" {
break
}
}
}

//get entity_id from list of identifiers
if id == "" {
for _, i := range identifiers {
id, _ = i.(string)
if id != "" {
break
}
if v, ok := i.(int); ok {
id = strconv.Itoa(v)
break
}
//use the entity id and sequence no. to get the entity if they were passed in
if entityID != "" {
//get the entity using the sequence no.
if seqInt != 0 {
r, er := model.GetContentBySequenceNumber(app.EventRepository(), entityID, int64(seqInt))
err = er
if r.SequenceNo == 0 {
return NewControllerError("No entity found", err, http.StatusNotFound)
}
}
//if sequence number given, get entity by sequence number
if sequence != 0 {
r, er := model.GetContentBySequenceNumber(app.EventRepository(), id, int64(sequence))
if r != nil && r.SequenceNo != 0 {
if r != nil && r.ID != "" {
if r != nil && r.ID != "" { //get the map from the entity
if r.Property != nil {
result = r.Property.(map[string]interface{})
} else {
result = make(map[string]interface{})
}
if err == nil && r.SequenceNo < int64(sequence) && r.ID != "" {
return ctxt.JSON(http.StatusNotModified, result)
}

}
result["weos_id"] = r.ID
result["sequence_no"] = r.SequenceNo
err = er
if err == nil && r.SequenceNo < int64(seqInt) && etag != "" { //if the etag is set then let's return the header
return ctxt.JSON(http.StatusNotModified, r.Property)
}
} else {
//get entity by entity_id
for _, projection := range app.Projections() {
if projection != nil {
result, err = projection.GetByEntityID(ctxt.Request().Context(), *cType, id)
result, err = projection.GetByEntityID(ctxt.Request().Context(), *cType, entityID)
}
}
}
}

weos_id, ok := result["weos_id"].(string)
if errors.Is(err, gorm.ErrRecordNotFound) || (len(result) == 0) || !ok || weos_id == "" {
return NewControllerError("No entity found", err, http.StatusNotFound)
Expand Down
206 changes: 182 additions & 24 deletions controllers/rest/controller_standard_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -528,37 +528,74 @@ func TestStandardControllers_View(t *testing.T) {
return nil
},
}

projection := &ProjectionMock{
GetByKeyFunc: func(ctxt context.Context, contentType weoscontext.ContentType, identifiers map[string]interface{}) (map[string]interface{}, error) {
return map[string]interface{}{
"id": "1234sd",
"weos_id": "1234sd",
}, nil
},
GetByEntityIDFunc: func(ctxt context.Context, contentType weoscontext.ContentType, id string) (map[string]interface{}, error) {
return map[string]interface{}{
"id": "1234sd",
"weos_id": "1234sd",
}, nil
mockEvent1 := &model.Event{
ID: "1234sd",
Type: "create",
Payload: nil,
Meta: model.EventMeta{
EntityID: "1234sd",
EntityType: "Blog",
SequenceNo: 1,
User: "",
Module: "",
RootID: "",
Group: "",
Created: "",
},
Version: 0,
}

application := &ApplicationMock{
DispatcherFunc: func() model.Dispatcher {
return dispatcher
},
ProjectionsFunc: func() []model.Projection {
return []model.Projection{projection}
mockEvent2 := &model.Event{
ID: "1234sd",
Type: "update",
Payload: nil,
Meta: model.EventMeta{
EntityID: "1234sd",
EntityType: "Blog",
SequenceNo: 2,
User: "",
Module: "",
RootID: "",
Group: "",
Created: "",
},
Version: 0,
}
eventRepository := &EventRepositoryMock{GetByAggregateAndSequenceRangeFunc: func(ID string, start int64, end int64) ([]*model.Event, error) {
return []*model.Event{mockEvent1, mockEvent2}, nil
}}

t.Run("Testing the generic view endpoint", func(t *testing.T) {
projection := &ProjectionMock{
GetByKeyFunc: func(ctxt context.Context, contentType weoscontext.ContentType, identifiers map[string]interface{}) (map[string]interface{}, error) {
return map[string]interface{}{
"id": "1",
"weos_id": "1234sd",
}, nil
},
GetByEntityIDFunc: func(ctxt context.Context, contentType weoscontext.ContentType, id string) (map[string]interface{}, error) {
return map[string]interface{}{
"id": "1",
"weos_id": "1234sd",
}, nil
},
}
application := &ApplicationMock{
DispatcherFunc: func() model.Dispatcher {
return dispatcher
},
ProjectionsFunc: func() []model.Projection {
return []model.Projection{projection}
},
EventRepositoryFunc: func() model.EventRepository {
return eventRepository
},
}

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

t.Run("Testing the generic list endpoint", func(t *testing.T) {
paramName := "id"
paramValue := "1234sd"
paramValue := "1"
path := swagger.Paths.Find("/blogs/:" + paramName)
controller := restAPI.View(restAPI.Application, swagger, path, path.Get)
resp := httptest.NewRecorder()
Expand All @@ -570,6 +607,127 @@ func TestStandardControllers_View(t *testing.T) {
response := resp.Result()
defer response.Body.Close()

//confirm the projection is called
if len(projection.GetByKeyCalls()) != 1 {
t.Errorf("expected the get by key method on the projection to be called %d time, called %d times", 1, len(projection.GetByKeyCalls()))
}

if response.StatusCode != 200 {
t.Errorf("expected response code to be %d, got %d", 200, response.StatusCode)
}
})
t.Run("Testing view with entity id", func(t *testing.T) {
projection := &ProjectionMock{
GetByKeyFunc: func(ctxt context.Context, contentType weoscontext.ContentType, identifiers map[string]interface{}) (map[string]interface{}, error) {
return map[string]interface{}{
"id": "1",
"weos_id": "1234sd",
}, nil
},
GetByEntityIDFunc: func(ctxt context.Context, contentType weoscontext.ContentType, id string) (map[string]interface{}, error) {
return map[string]interface{}{
"id": "1",
"weos_id": "1234sd",
}, nil
},
}
application := &ApplicationMock{
DispatcherFunc: func() model.Dispatcher {
return dispatcher
},
ProjectionsFunc: func() []model.Projection {
return []model.Projection{projection}
},
EventRepositoryFunc: func() model.EventRepository {
return eventRepository
},
}

//initialization will instantiate with application so we need to overwrite with our mock application
restAPI.Application = application
paramName := "id"
paramValue := "1234sd"
path := swagger.Paths.Find("/blogs/:" + paramName)
if path == nil {
t.Fatalf("could not find path '%s' in routes", "/blogs/{"+paramName+"}")
}
controller := restAPI.View(restAPI.Application, swagger, path, path.Get)
resp := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/blogs/"+paramValue+"?use_entity_id=true", nil)
mw := rest.Context(restAPI.Application, swagger, path, path.Get)
e.GET("/blogs/:"+paramName, controller, mw)
e.ServeHTTP(resp, req)

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

//confirm the entity is retrieved by entity id
if len(projection.GetByEntityIDCalls()) != 1 {
t.Errorf("expected the get by key method on the projection to be called %d time, called %d times", 1, len(projection.GetByEntityIDCalls()))
}

if len(projection.GetByKeyCalls()) != 0 {
t.Errorf("expected the get by key method on the projection to be called %d times, called %d times", 0, len(projection.GetByKeyCalls()))
}

if response.StatusCode != 200 {
t.Errorf("expected response code to be %d, got %d", 200, response.StatusCode)
}
})
t.Run("Testing view with sequence no", func(t *testing.T) {
projection := &ProjectionMock{
GetByKeyFunc: func(ctxt context.Context, contentType weoscontext.ContentType, identifiers map[string]interface{}) (map[string]interface{}, error) {
return map[string]interface{}{
"id": "1",
"weos_id": "1234sd",
}, nil
},
GetByEntityIDFunc: func(ctxt context.Context, contentType weoscontext.ContentType, id string) (map[string]interface{}, error) {
return map[string]interface{}{
"id": "1",
"weos_id": "1234sd",
}, nil
},
}
application := &ApplicationMock{
DispatcherFunc: func() model.Dispatcher {
return dispatcher
},
ProjectionsFunc: func() []model.Projection {
return []model.Projection{projection}
},
EventRepositoryFunc: func() model.EventRepository {
return eventRepository
},
}

//initialization will instantiate with application so we need to overwrite with our mock application
restAPI.Application = application
paramName := "id"
paramValue := "1234sd"
path := swagger.Paths.Find("/blogs/:" + paramName)
if path == nil {
t.Fatalf("could not find path '%s' in swagger paths", "/blogs/:"+paramName)
}
controller := restAPI.View(restAPI.Application, swagger, path, path.Get)
resp := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/blogs/"+paramValue+"?sequence_no=1", nil)
mw := rest.Context(restAPI.Application, swagger, path, path.Get)
e.GET("/blogs/:"+paramName, controller, mw)
e.ServeHTTP(resp, req)

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

//confirm the entity is retrieved to get entity id
if len(projection.GetByKeyCalls()) != 1 {
t.Errorf("expected the get by key method on the projection to be called %d time, called %d times", 1, len(projection.GetByKeyCalls()))
}

if len(eventRepository.GetByAggregateAndSequenceRangeCalls()) != 1 {
t.Errorf("expected the event repository to be called %d time, called %d times", 1, len(eventRepository.GetByAggregateAndSequenceRangeCalls()))
}

if response.StatusCode != 200 {
t.Errorf("expected response code to be %d, got %d", 200, response.StatusCode)
}
Expand Down
9 changes: 9 additions & 0 deletions controllers/rest/fixtures/blog.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,15 @@ paths:
items:
$ref: "#/components/schemas/Blog"
/blogs/{id}:
parameters:
- in: query
name: sequence_no
schema:
type: integer
- in: query
name: use_entity_id
schema:
type: boolean
get:
parameters:
- in: path
Expand Down
1 change: 1 addition & 0 deletions end2end_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -919,6 +919,7 @@ func TestBDD(t *testing.T) {
Options: &godog.Options{
Format: "pretty",
Tags: "~skipped && ~long",
//Tags: "WEOS-1310",
},
}.Run()
if status != 0 {
Expand Down
Loading