diff --git a/docs/res-client-protocol.md b/docs/res-client-protocol.md index 4a2f8f1..7c10e98 100644 --- a/docs/res-client-protocol.md +++ b/docs/res-client-protocol.md @@ -50,7 +50,7 @@ The resource that is subscribed to with a [subscribe request](#subscribe-request It is possible to make multiple direct subscriptions on a resource. It will be considered directly subscribed until an equal number of [unsubscribe requests](#unsubscribe-request) has been made. ## Indirect subscription -A resource that is referred to with a [resource reference](res-protocol.md#values) by a [directly subscribed](#direct-subscription) resource, or by an indirectly subscribed resource, will be considered *indirectly subscribed*. Cyclic references where none of the resources are directly subscribed will not be considered subscribed. +A resource that is referred to with a non-soft [resource reference](res-protocol.md#values) by a [directly subscribed](#direct-subscription) resource, or by an indirectly subscribed resource, will be considered *indirectly subscribed*. Cyclic references where none of the resources are directly subscribed will not be considered subscribed. ## Resource set diff --git a/docs/res-protocol.md b/docs/res-protocol.md index f2cd9d7..eba32a4 100644 --- a/docs/res-protocol.md +++ b/docs/res-protocol.md @@ -94,12 +94,17 @@ null // null ## Resource references -A resource reference is a JSON objects with the following parameter: +A resource reference is a link to a resource. A *soft reference* is a resource reference which will not automatically be followed by the gateway. The resource reference is a JSON objects with the following parameters: **rid** Resource ID of the referenced resource. MUST be a valid [resource ID](#resource-ids). +**soft** +Flag telling if the reference is a soft resource reference. +May be omitted if the reference is not a soft reference. +MUST be a boolean. + ## Messaging system The messaging system handles the communication between [services](#services) and [gateways](#gateways). It MUST provide the following functionality: diff --git a/docs/res-service-protocol.md b/docs/res-service-protocol.md index 17dc038..f96bc36 100644 --- a/docs/res-service-protocol.md +++ b/docs/res-service-protocol.md @@ -152,14 +152,16 @@ MUST be a string. ### Result **get** -Flag if the client has access to get (read) the resource. +Flag telling if the client has access to get (read) the resource, including any +resource recursively referenced by non-soft [resource +references](res-protocol.md#resource-references). May be omitted if client has no get access. -MUST be a boolean +MUST be a boolean. **call** A comma separated list of methods that the client can call. Eg. `"set,foo,bar"`. May be omitted if client is not allowed to call any methods. -Value may be a single asterisk character (`"*"`) if client is allowed to call any method. +Value may be a single asterisk character (`"*"`) if client is allowed to call any method. ### Error diff --git a/server/apiEncoding.go b/server/apiEncoding.go index a36eeed..0d133f2 100644 --- a/server/apiEncoding.go +++ b/server/apiEncoding.go @@ -214,13 +214,8 @@ func (e *encoderJSON) encodeSubscription(s *Subscription, wrap bool) error { if i > 0 { e.b.WriteByte(',') } - if v.Type == codec.ValueTypeResource { - sc := s.Ref(v.RID) - if err := e.encodeSubscription(sc, true); err != nil { - return err - } - } else { - e.b.Write(v.RawMessage) + if err := e.encodeValue(s, v); err != nil { + return err } } e.b.WriteByte(']') @@ -247,13 +242,8 @@ func (e *encoderJSON) encodeSubscription(s *Subscription, wrap bool) error { e.b.Write(dta) e.b.WriteByte(':') - if v.Type == codec.ValueTypeResource { - sc := s.Ref(v.RID) - if err := e.encodeSubscription(sc, true); err != nil { - return err - } - } else { - e.b.Write(v.RawMessage) + if err := e.encodeValue(s, v); err != nil { + return err } } e.b.WriteByte('}') @@ -265,6 +255,27 @@ func (e *encoderJSON) encodeSubscription(s *Subscription, wrap bool) error { return nil } +func (e *encoderJSON) encodeValue(s *Subscription, v codec.Value) error { + switch v.Type { + case codec.ValueTypeReference: + sc := s.Ref(v.RID) + if err := e.encodeSubscription(sc, true); err != nil { + return err + } + case codec.ValueTypeSoftReference: + e.b.Write([]byte(`{"href":`)) + dta, err := json.Marshal(RIDToPath(v.RID, e.apiPath)) + if err != nil { + return err + } + e.b.Write(dta) + e.b.WriteByte('}') + default: + e.b.Write(v.RawMessage) + } + return nil +} + type encoderJSONFlat struct { b bytes.Buffer path []string @@ -338,13 +349,8 @@ func (e *encoderJSONFlat) encodeSubscription(s *Subscription) error { if i > 0 { e.b.WriteByte(',') } - if v.Type == codec.ValueTypeResource { - sc := s.Ref(v.RID) - if err := e.encodeSubscription(sc); err != nil { - return err - } - } else { - e.b.Write(v.RawMessage) + if err := e.encodeValue(s, v); err != nil { + return err } } e.b.WriteByte(']') @@ -368,13 +374,8 @@ func (e *encoderJSONFlat) encodeSubscription(s *Subscription) error { e.b.Write(dta) e.b.WriteByte(':') - if v.Type == codec.ValueTypeResource { - sc := s.Ref(v.RID) - if err := e.encodeSubscription(sc); err != nil { - return err - } - } else { - e.b.Write(v.RawMessage) + if err := e.encodeValue(s, v); err != nil { + return err } } e.b.WriteByte('}') @@ -385,6 +386,27 @@ func (e *encoderJSONFlat) encodeSubscription(s *Subscription) error { return nil } +func (e *encoderJSONFlat) encodeValue(s *Subscription, v codec.Value) error { + switch v.Type { + case codec.ValueTypeReference: + sc := s.Ref(v.RID) + if err := e.encodeSubscription(sc); err != nil { + return err + } + case codec.ValueTypeSoftReference: + e.b.Write([]byte(`{"href":`)) + dta, err := json.Marshal(RIDToPath(v.RID, e.apiPath)) + if err != nil { + return err + } + e.b.Write(dta) + e.b.WriteByte('}') + default: + e.b.Write(v.RawMessage) + } + return nil +} + func jsonEncodeError(rerr *reserr.Error) []byte { out, err := json.Marshal(rerr) if err != nil { diff --git a/server/codec/codec.go b/server/codec/codec.go index f44959d..c585a4a 100644 --- a/server/codec/codec.go +++ b/server/codec/codec.go @@ -175,9 +175,10 @@ type ValueType byte // Value type constants const ( ValueTypeNone ValueType = iota - ValueTypePrimitive - ValueTypeResource ValueTypeDelete + ValueTypePrimitive + ValueTypeReference + ValueTypeSoftReference ) // Value represents a RES value @@ -191,9 +192,16 @@ type Value struct { // ValueObject represents a resource reference or an action type ValueObject struct { RID *string `json:"rid"` + Soft bool `json:"soft"` Action *string `json:"action"` } +// IsProper returns true if the value's type is either a primitive or a +// reference. +func (v Value) IsProper() bool { + return v.Type >= ValueTypePrimitive +} + // DeleteValue is a predeclared delete action value var DeleteValue = Value{ RawMessage: json.RawMessage(`{"action":"delete"}`), @@ -231,11 +239,15 @@ func (v *Value) UnmarshalJSON(data []byte) error { if mvo.Action != nil || *mvo.RID == "" { return errInvalidValue } - v.Type = ValueTypeResource v.RID = *mvo.RID if !IsValidRID(v.RID, true) { return errInvalidValue } + if mvo.Soft { + v.Type = ValueTypeSoftReference + } else { + v.Type = ValueTypeReference + } } else { // Must be an action of type actionDelete if mvo.Action == nil || *mvo.Action != actionDelete { @@ -261,7 +273,9 @@ func (v Value) Equal(w Value) bool { switch v.Type { case ValueTypePrimitive: return bytes.Equal(v.RawMessage, w.RawMessage) - case ValueTypeResource: + case ValueTypeReference: + fallthrough + case ValueTypeSoftReference: return v.RID == w.RID } @@ -320,14 +334,14 @@ func DecodeGetResponse(payload []byte) (*GetResult, error) { } // Assert model only has proper values for _, v := range res.Model { - if v.Type != ValueTypeResource && v.Type != ValueTypePrimitive { + if !v.IsProper() { return nil, errInvalidResponse } } } else if res.Collection != nil { // Assert collection only has proper values for _, v := range res.Collection { - if v.Type != ValueTypeResource && v.Type != ValueTypePrimitive { + if !v.IsProper() { return nil, errInvalidResponse } } @@ -397,14 +411,14 @@ func DecodeEventQueryResponse(payload []byte) (*EventQueryResult, error) { } // Assert model only has proper values for _, v := range res.Model { - if v.Type != ValueTypeResource && v.Type != ValueTypePrimitive { + if !v.IsProper() { return nil, errInvalidResponse } } case res.Collection != nil: // Assert collection only has proper values for _, v := range res.Collection { - if v.Type != ValueTypeResource && v.Type != ValueTypePrimitive { + if !v.IsProper() { return nil, errInvalidResponse } } @@ -483,8 +497,7 @@ func DecodeAddEvent(data json.RawMessage) (*AddEvent, error) { } // Assert it is a proper value - t := d.Value.Type - if t != ValueTypeResource && t != ValueTypePrimitive { + if !d.Value.IsProper() { return nil, errInvalidValue } diff --git a/server/rescache/legacy.go b/server/rescache/legacy.go new file mode 100644 index 0000000..1831429 --- /dev/null +++ b/server/rescache/legacy.go @@ -0,0 +1,88 @@ +package rescache + +import ( + "bytes" + "encoding/json" + + "github.com/resgateio/resgate/server/codec" +) + +// Legacy120Model marshals a model compatible with version 1.2.0 +// (versionSoftResourceReference) and below. +type Legacy120Model Model + +// Legacy120Collection marshals a collection compatible with version 1.2.0 +// (versionSoftResourceReference) and below. +type Legacy120Collection Collection + +// Legacy120Value marshals a value compatible with version 1.2.0 +// (versionSoftResourceReference) and below. +type Legacy120Value codec.Value + +// Legacy120ValueMap marshals a map of values compatible with version 1.2.0 +// (versionSoftResourceReference) and below. +type Legacy120ValueMap map[string]codec.Value + +// MarshalJSON creates a JSON encoded representation of the model +func (m *Legacy120Model) MarshalJSON() ([]byte, error) { + for _, v := range m.Values { + if v.Type == codec.ValueTypeSoftReference { + return Legacy120ValueMap(m.Values).MarshalJSON() + } + } + return (*Model)(m).MarshalJSON() +} + +// MarshalJSON creates a JSON encoded representation of the model +func (c *Legacy120Collection) MarshalJSON() ([]byte, error) { + for _, v := range c.Values { + if v.Type == codec.ValueTypeSoftReference { + goto LegacyMarshal + } + } + return (*Collection)(c).MarshalJSON() + +LegacyMarshal: + + vs := c.Values + lvs := make([]Legacy120Value, len(vs)) + for i, v := range vs { + lvs[i] = Legacy120Value(v) + } + + return json.Marshal(lvs) +} + +// MarshalJSON creates a JSON encoded representation of the value +func (v Legacy120Value) MarshalJSON() ([]byte, error) { + if v.Type == codec.ValueTypeSoftReference { + return json.Marshal(v.RID) + } + return v.RawMessage, nil +} + +// MarshalJSON creates a JSON encoded representation of the map +func (m Legacy120ValueMap) MarshalJSON() ([]byte, error) { + var b bytes.Buffer + notfirst := false + b.WriteByte('{') + for k, v := range m { + if notfirst { + b.WriteByte(',') + } + notfirst = true + dta, err := json.Marshal(k) + if err != nil { + return nil, err + } + b.Write(dta) + b.WriteByte(':') + dta, err = Legacy120Value(v).MarshalJSON() + if err != nil { + return nil, err + } + b.Write(dta) + } + b.WriteByte('}') + return b.Bytes(), nil +} diff --git a/server/rescache/legacy_test.go b/server/rescache/legacy_test.go new file mode 100644 index 0000000..624d0bd --- /dev/null +++ b/server/rescache/legacy_test.go @@ -0,0 +1,104 @@ +package rescache_test + +import ( + "encoding/json" + "fmt" + "reflect" + "testing" + + "github.com/resgateio/resgate/server/codec" + "github.com/resgateio/resgate/server/rescache" +) + +func TestLegacy120Model_MarshalJSON_ReturnsSoftReferenceAsString(t *testing.T) { + var v map[string]codec.Value + dta := []byte(`{"name":"softparent","child":{"rid":"test.model","soft":true}}`) + expected := []byte(`{"name":"softparent","child":"test.model"}`) + err := json.Unmarshal(dta, &v) + if err != nil { + t.Fatal(err) + } + m := &rescache.Model{Values: v} + lm := (*rescache.Legacy120Model)(m) + + out, err := lm.MarshalJSON() + if err != nil { + t.Fatal(err) + } + + AssertEqualJSON(t, "Legacy120Model.MarshalJSON", json.RawMessage(out), json.RawMessage(expected)) +} + +func TestLegacy120Collection_MarshalJSON_ReturnsSoftReferenceAsString(t *testing.T) { + var v []codec.Value + dta := []byte(`["softparent",{"rid":"test.model","soft":true}]`) + expected := []byte(`["softparent","test.model"]`) + err := json.Unmarshal(dta, &v) + if err != nil { + t.Fatal(err) + } + m := &rescache.Collection{Values: v} + lm := (*rescache.Legacy120Collection)(m) + + out, err := lm.MarshalJSON() + if err != nil { + t.Fatal(err) + } + + AssertEqualJSON(t, "Legacy120Collection.MarshalJSON", json.RawMessage(out), json.RawMessage(expected)) +} + +func TestLegacy120Value_MarshalJSON__ReturnsSoftReferenceAsString(t *testing.T) { + var v codec.Value + dta := []byte(`{"rid":"test.model","soft":true}`) + expected := []byte(`"test.model"`) + err := json.Unmarshal(dta, &v) + if err != nil { + t.Fatal(err) + } + lv := rescache.Legacy120Value(v) + + out, err := lv.MarshalJSON() + if err != nil { + t.Fatal(err) + } + + AssertEqualJSON(t, "Legacy120Value.MarshalJSON", json.RawMessage(out), json.RawMessage(expected)) +} + +// AssertEqualJSON expects that a and b json marshals into equal values, and +// returns true if they do, otherwise logs a fatal error and returns false. +func AssertEqualJSON(t *testing.T, name string, result, expected interface{}, ctx ...interface{}) bool { + aa, aj := jsonMap(t, result) + bb, bj := jsonMap(t, expected) + + if !reflect.DeepEqual(aa, bb) { + t.Fatalf("expected %s to be:\n\t%s\nbut got:\n\t%s%s", name, bj, aj, ctxString(ctx)) + return false + } + + return true +} + +func ctxString(ctx []interface{}) string { + if len(ctx) == 0 { + return "" + } + return "\nin " + fmt.Sprint(ctx...) +} + +func jsonMap(t *testing.T, v interface{}) (interface{}, []byte) { + var err error + j, err := json.Marshal(v) + if err != nil { + panic(fmt.Sprintf("test: error marshaling value:\n\t%+v\nerror:\n\t%s", v, err)) + } + + var m interface{} + err = json.Unmarshal(j, &m) + if err != nil { + panic(fmt.Sprintf("test: error unmarshaling value:\n\t%s\nerror:\n\t%s", j, err)) + } + + return m, j +} diff --git a/server/rescache/resourceSubscription.go b/server/rescache/resourceSubscription.go index 4e032f0..af089c4 100644 --- a/server/rescache/resourceSubscription.go +++ b/server/rescache/resourceSubscription.go @@ -2,6 +2,7 @@ package rescache import ( "encoding/json" + "errors" "github.com/resgateio/resgate/server/codec" "github.com/resgateio/resgate/server/reserr" @@ -453,6 +454,9 @@ func (rs *ResourceSubscription) processResetGetResponse(payload []byte, err erro // or an error in the service's response if err == nil { result, err = codec.DecodeGetResponse(payload) + if err == nil && ((rs.state == stateModel && result.Model == nil) || (rs.state == stateCollection && result.Collection == nil)) { + err = errors.New("mismatching resource type") + } } // Get request failed diff --git a/server/subscription.go b/server/subscription.go index c959011..5b6f942 100644 --- a/server/subscription.go +++ b/server/subscription.go @@ -27,6 +27,7 @@ type ConnSubscriber interface { Enqueue(f func()) bool ExpandCID(string) string Disconnect(reason string) + ProtocolVersion() int } // Subscription represents a resource subscription made by a client connection @@ -265,7 +266,11 @@ func (s *Subscription) onLoaded(rcb *readyCallback) { // It will lock the subscription and queue any events until ReleaseRPCResources is called. func (s *Subscription) GetRPCResources() *rpc.Resources { r := &rpc.Resources{} - s.populateResources(r) + if s.c.ProtocolVersion() <= versionSoftResourceReference { + s.populateResourcesLegacy(r) + } else { + s.populateResources(r) + } return r } @@ -358,6 +363,48 @@ func (s *Subscription) populateResources(r *rpc.Resources) { } } +// populateResourcesLegacy is the same as populateResources, but uses legacy +// encodings of resources. +func (s *Subscription) populateResourcesLegacy(r *rpc.Resources) { + // Quick exit if resource is already sent + if s.state == stateSent || s.state == stateToSend { + return + } + + // Check for errors + err := s.Error() + if err != nil { + // Create Errors map if needed + if r.Errors == nil { + r.Errors = make(map[string]*reserr.Error) + } + r.Errors[s.rid] = reserr.RESError(err) + return + } + + switch s.typ { + case rescache.TypeCollection: + // Create Collections map if needed + if r.Collections == nil { + r.Collections = make(map[string]interface{}) + } + r.Collections[s.rid] = (*rescache.Legacy120Collection)(s.collection) + + case rescache.TypeModel: + // Create Models map if needed + if r.Models == nil { + r.Models = make(map[string]interface{}) + } + r.Models[s.rid] = (*rescache.Legacy120Model)(s.model) + } + + s.state = stateToSend + + for _, sc := range s.refs { + sc.sub.populateResourcesLegacy(r) + } +} + // setModel subscribes to all resource references in the model. func (s *Subscription) setModel() { m := s.resourceSub.GetModel() @@ -390,7 +437,7 @@ func (s *Subscription) setCollection() { // be unsubscribed, s.err set, s.doneLoading called, and false returned. // If v is not a resource reference, nothing will happen. func (s *Subscription) subscribeRef(v codec.Value) bool { - if v.Type != codec.ValueTypeResource { + if v.Type != codec.ValueTypeReference { return true } @@ -527,7 +574,7 @@ func (s *Subscription) processCollectionEvent(event *rescache.ResourceEvent) { idx := event.Idx switch v.Type { - case codec.ValueTypeResource: + case codec.ValueTypeReference: rid := v.RID sub, err := s.addReference(rid) if err != nil { @@ -558,6 +605,12 @@ func (s *Subscription) processCollectionEvent(event *rescache.ResourceEvent) { s.unqueueEvents(queueReasonLoading) }) + case codec.ValueTypeSoftReference: + if s.c.ProtocolVersion() <= versionSoftResourceReference { + s.c.Send(rpc.NewEvent(s.rid, event.Event, rpc.AddEvent{Idx: idx, Value: v.RID})) + break + } + fallthrough case codec.ValueTypePrimitive: s.c.Send(rpc.NewEvent(s.rid, event.Event, rpc.AddEvent{Idx: idx, Value: v.RawMessage})) } @@ -566,7 +619,7 @@ func (s *Subscription) processCollectionEvent(event *rescache.ResourceEvent) { // Remove and unsubscribe to model v := event.Value - if v.Type == codec.ValueTypeResource { + if v.Type == codec.ValueTypeReference { s.removeReference(v.RID) } s.c.Send(rpc.NewEvent(s.rid, event.Event, event.Payload)) @@ -587,7 +640,7 @@ func (s *Subscription) processModelEvent(event *rescache.ResourceEvent) { var subs []*Subscription for _, v := range ch { - if v.Type == codec.ValueTypeResource { + if v.Type == codec.ValueTypeReference { sub, err := s.addReference(v.RID) if err != nil { s.c.Errorf("Subscription %s: Error subscribing to resource %s: %s", s.rid, v.RID, err) @@ -606,14 +659,19 @@ func (s *Subscription) processModelEvent(event *rescache.ResourceEvent) { // Check for removing changed references after adding references to avoid unsubscribing to // a resource that is going to be subscribed again because it has moved between properties. for k := range ch { - if ov, ok := old[k]; ok && ov.Type == codec.ValueTypeResource { + if ov, ok := old[k]; ok && ov.Type == codec.ValueTypeReference { s.removeReference(ov.RID) } } // Quick exit if there are no new unsent subscriptions if subs == nil { - s.c.Send(rpc.NewEvent(s.rid, event.Event, rpc.ChangeEvent{Values: event.Changed})) + // Legacy behavior + if s.c.ProtocolVersion() <= versionSoftResourceReference { + s.c.Send(rpc.NewEvent(s.rid, event.Event, rpc.ChangeEvent{Values: rescache.Legacy120ValueMap(event.Changed)})) + } else { + s.c.Send(rpc.NewEvent(s.rid, event.Event, rpc.ChangeEvent{Values: event.Changed})) + } return } @@ -633,10 +691,19 @@ func (s *Subscription) processModelEvent(event *rescache.ResourceEvent) { } r := &rpc.Resources{} - for _, sub := range subs { - sub.populateResources(r) + + // Legacy behavior + if s.c.ProtocolVersion() <= versionSoftResourceReference { + for _, sub := range subs { + sub.populateResourcesLegacy(r) + } + s.c.Send(rpc.NewEvent(s.rid, event.Event, rpc.ChangeEvent{Values: rescache.Legacy120ValueMap(event.Changed), Resources: r})) + } else { + for _, sub := range subs { + sub.populateResources(r) + } + s.c.Send(rpc.NewEvent(s.rid, event.Event, rpc.ChangeEvent{Values: event.Changed, Resources: r})) } - s.c.Send(rpc.NewEvent(s.rid, event.Event, rpc.ChangeEvent{Values: event.Changed, Resources: r})) for _, sub := range subs { sub.ReleaseRPCResources() } diff --git a/server/version.go b/server/version.go index 5558a7c..fd663c6 100644 --- a/server/version.go +++ b/server/version.go @@ -2,5 +2,6 @@ package server // Last protocol version where a specific feature was not supported. const ( - versionCallResourceResponse = 1001001 + versionCallResourceResponse = 1001001 + versionSoftResourceReference = 1002000 ) diff --git a/test/01subscribe_test.go b/test/01subscribe_test.go index f155315..c0b01c0 100644 --- a/test/01subscribe_test.go +++ b/test/01subscribe_test.go @@ -185,118 +185,144 @@ func TestResponseOnPrimitiveModelRetrieval(t *testing.T) { func TestSubscribe(t *testing.T) { event := json.RawMessage(`{"foo":"bar"}`) - responses := map[string][]string{ - // Model responses - "test.model": {"test.model"}, - "test.model.parent": {"test.model.parent", "test.model"}, - "test.model.grandparent": {"test.model.grandparent", "test.model.parent", "test.model"}, - "test.model.secondparent": {"test.model.secondparent", "test.model"}, - "test.model.brokenchild": {"test.model.brokenchild", "test.err.notFound"}, - // Cyclic model responses - "test.m.a": {"test.m.a"}, - "test.m.b": {"test.m.b", "test.m.c"}, - "test.m.d": {"test.m.d", "test.m.e", "test.m.f"}, - "test.m.g": {"test.m.d", "test.m.e", "test.m.f", "test.m.g"}, - "test.m.h": {"test.m.d", "test.m.e", "test.m.f", "test.m.h"}, - // Collection responses - "test.collection": {"test.collection"}, - "test.collection.parent": {"test.collection.parent", "test.collection"}, - "test.collection.grandparent": {"test.collection.grandparent", "test.collection.parent", "test.collection"}, - "test.collection.secondparent": {"test.collection.secondparent", "test.collection"}, - "test.collection.brokenchild": {"test.collection.brokenchild", "test.err.notFound"}, - // Cyclic collection responses - "test.c.a": {"test.c.a"}, - "test.c.b": {"test.c.b", "test.c.c"}, - "test.c.d": {"test.c.d", "test.c.e", "test.c.f"}, - "test.c.g": {"test.c.d", "test.c.e", "test.c.f", "test.c.g"}, - "test.c.h": {"test.c.d", "test.c.e", "test.c.f", "test.c.h"}, + responses := map[string]map[string][]struct { + RID string + Resource *resource + }{ + versionLatest: { + // Model responses + "test.model": {{"test.model", nil}}, + "test.model.parent": {{"test.model.parent", nil}, {"test.model", nil}}, + "test.model.grandparent": {{"test.model.grandparent", nil}, {"test.model.parent", nil}, {"test.model", nil}}, + "test.model.secondparent": {{"test.model.secondparent", nil}, {"test.model", nil}}, + "test.model.brokenchild": {{"test.model.brokenchild", nil}, {"test.err.notFound", nil}}, + "test.model.soft": {{"test.model.soft", nil}}, + "test.model.soft.parent": {{"test.model.soft.parent", nil}, {"test.model.soft", nil}}, + // Cyclic model responses + "test.m.a": {{"test.m.a", nil}}, + "test.m.b": {{"test.m.b", nil}, {"test.m.c", nil}}, + "test.m.d": {{"test.m.d", nil}, {"test.m.e", nil}, {"test.m.f", nil}}, + "test.m.g": {{"test.m.d", nil}, {"test.m.e", nil}, {"test.m.f", nil}, {"test.m.g", nil}}, + "test.m.h": {{"test.m.d", nil}, {"test.m.e", nil}, {"test.m.f", nil}, {"test.m.h", nil}}, + // Collection responses + "test.collection": {{"test.collection", nil}}, + "test.collection.parent": {{"test.collection.parent", nil}, {"test.collection", nil}}, + "test.collection.grandparent": {{"test.collection.grandparent", nil}, {"test.collection.parent", nil}, {"test.collection", nil}}, + "test.collection.secondparent": {{"test.collection.secondparent", nil}, {"test.collection", nil}}, + "test.collection.brokenchild": {{"test.collection.brokenchild", nil}, {"test.err.notFound", nil}}, + "test.collection.soft": {{"test.collection.soft", nil}}, + "test.collection.soft.parent": {{"test.collection.soft.parent", nil}, {"test.collection.soft", nil}}, + // Cyclic collection responses + "test.c.a": {{"test.c.a", nil}}, + "test.c.b": {{"test.c.b", nil}, {"test.c.c", nil}}, + "test.c.d": {{"test.c.d", nil}, {"test.c.e", nil}, {"test.c.f", nil}}, + "test.c.g": {{"test.c.d", nil}, {"test.c.e", nil}, {"test.c.f", nil}, {"test.c.g", nil}}, + "test.c.h": {{"test.c.d", nil}, {"test.c.e", nil}, {"test.c.f", nil}, {"test.c.h", nil}}, + }, + "1.2.0": { + // Model responses + "test.model.soft": {{"test.model.soft", &resource{typeModel, `{"name":"soft","child":"test.model"}`, nil}}}, + "test.model.soft.parent": {{"test.model.soft.parent", nil}, {"test.model.soft", &resource{typeModel, `{"name":"soft","child":"test.model"}`, nil}}}, + // Collection responses + "test.collection.soft": {{"test.collection.soft", &resource{typeCollection, `["soft","test.collection"]`, nil}}}, + "test.collection.soft.parent": {{"test.collection.soft.parent", nil}, {"test.collection.soft", &resource{typeCollection, `["soft","test.collection"]`, nil}}}, + }, } - for i, l := range sequenceTable { - runNamedTest(t, fmt.Sprintf("#%d", i+1), func(s *Session) { - var creq *ClientRequest - var req *Request + for _, set := range sequenceSets { + for i, l := range set.Table { + runNamedTest(t, fmt.Sprintf("#%d for client version %s", i+1, set.Version), func(s *Session) { + var creq *ClientRequest + var req *Request - c := s.Connect() + c := s.ConnectWithVersion(set.Version) - creqs := make(map[string]*ClientRequest) - reqs := make(map[string]*Request) - sentResources := make(map[string]bool) + creqs := make(map[string]*ClientRequest) + reqs := make(map[string]*Request) + sentResources := make(map[string]bool) - for _, ev := range l { - switch ev.Event { - case "subscribe": - creqs[ev.RID] = c.Request("subscribe."+ev.RID, nil) - case "access": - for req = reqs["access."+ev.RID]; req == nil; req = reqs["access."+ev.RID] { - treq := s.GetRequest(t) - reqs[treq.Subject] = treq - } - req.RespondSuccess(json.RawMessage(`{"get":true}`)) - case "accessDenied": - for req = reqs["access."+ev.RID]; req == nil; req = reqs["access."+ev.RID] { - treq := s.GetRequest(t) - reqs[treq.Subject] = treq - } - req.RespondSuccess(json.RawMessage(`{"get":false}`)) - case "get": - for req = reqs["get."+ev.RID]; req == nil; req = reqs["get."+ev.RID] { - req = s.GetRequest(t) - reqs[req.Subject] = req - } - rsrc := resources[ev.RID] - switch rsrc.typ { - case typeModel: - req.RespondSuccess(json.RawMessage(`{"model":` + rsrc.data + `}`)) - case typeCollection: - req.RespondSuccess(json.RawMessage(`{"collection":` + rsrc.data + `}`)) - case typeError: - req.RespondError(rsrc.err) - } - case "response": - creq = creqs[ev.RID] - rids := responses[ev.RID] - models := make(map[string]json.RawMessage) - collections := make(map[string]json.RawMessage) - errors := make(map[string]*reserr.Error) - for _, rid := range rids { - if sentResources[rid] { - continue + for _, ev := range l { + switch ev.Event { + case "subscribe": + creqs[ev.RID] = c.Request("subscribe."+ev.RID, nil) + case "access": + for req = reqs["access."+ev.RID]; req == nil; req = reqs["access."+ev.RID] { + treq := s.GetRequest(t) + reqs[treq.Subject] = treq + } + req.RespondSuccess(json.RawMessage(`{"get":true}`)) + case "accessDenied": + for req = reqs["access."+ev.RID]; req == nil; req = reqs["access."+ev.RID] { + treq := s.GetRequest(t) + reqs[treq.Subject] = treq + } + req.RespondSuccess(json.RawMessage(`{"get":false}`)) + case "get": + for req = reqs["get."+ev.RID]; req == nil; req = reqs["get."+ev.RID] { + req = s.GetRequest(t) + reqs[req.Subject] = req } - rsrc := resources[rid] + rsrc := resources[ev.RID] switch rsrc.typ { case typeModel: - models[rid] = json.RawMessage(rsrc.data) + req.RespondSuccess(json.RawMessage(`{"model":` + rsrc.data + `}`)) case typeCollection: - collections[rid] = json.RawMessage(rsrc.data) + req.RespondSuccess(json.RawMessage(`{"collection":` + rsrc.data + `}`)) case typeError: - errors[rid] = rsrc.err + req.RespondError(rsrc.err) } - sentResources[rid] = true - } - m := make(map[string]interface{}) - if len(models) > 0 { - m["models"] = models - } - if len(collections) > 0 { - m["collections"] = collections - } - if len(errors) > 0 { - m["errors"] = errors + case "response": + creq = creqs[ev.RID] + respResources := responses[set.Version][ev.RID] + models := make(map[string]json.RawMessage) + collections := make(map[string]json.RawMessage) + errors := make(map[string]*reserr.Error) + for _, rr := range respResources { + if sentResources[rr.RID] { + continue + } + var rsrc resource + if rr.Resource == nil { + rsrc = resources[rr.RID] + } else { + rsrc = *rr.Resource + } + switch rsrc.typ { + case typeModel: + models[rr.RID] = json.RawMessage(rsrc.data) + case typeCollection: + collections[rr.RID] = json.RawMessage(rsrc.data) + case typeError: + errors[rr.RID] = rsrc.err + } + sentResources[rr.RID] = true + } + m := make(map[string]interface{}) + if len(models) > 0 { + m["models"] = models + } + if len(collections) > 0 { + m["collections"] = collections + } + if len(errors) > 0 { + m["errors"] = errors + } + creq.GetResponse(t).AssertResult(t, m) + case "errorResponse": + creq = creqs[ev.RID] + creq.GetResponse(t).AssertIsError(t) + case "event": + s.ResourceEvent(ev.RID, "custom", event) + c.GetEvent(t).Equals(t, ev.RID+".custom", event) + case "noevent": + s.ResourceEvent(ev.RID, "custom", event) + c.AssertNoEvent(t, ev.RID) + case "nosubscription": + s.NoSubscriptions(t, ev.RID) } - creq.GetResponse(t).AssertResult(t, m) - case "errorResponse": - creq = creqs[ev.RID] - creq.GetResponse(t).AssertIsError(t) - case "event": - s.ResourceEvent(ev.RID, "custom", event) - c.GetEvent(t).Equals(t, ev.RID+".custom", event) - case "noevent": - s.ResourceEvent(ev.RID, "custom", event) - c.AssertNoEvent(t, ev.RID) } - } - }) + }) + } } } diff --git a/test/07model_event_test.go b/test/07model_event_test.go index b6071b5..a01edaa 100644 --- a/test/07model_event_test.go +++ b/test/07model_event_test.go @@ -46,27 +46,32 @@ func TestChangeEventPriorToGetResponseIsDiscarded(t *testing.T) { // Test change event effect on cached model func TestChangeEventOnCachedModel(t *testing.T) { tbl := []struct { + RID string // RID of resource to subscribe to ChangeEvent string // Change event to send (raw JSON) ExpectedChangeEvent string // Expected event sent to client (raw JSON. Empty means none) ExpectedModel string // Expected model after event (raw JSON) ExpectedErrors int }{ - {`{"values":{"string":"bar","int":-12}}`, `{"values":{"string":"bar","int":-12}}`, `{"string":"bar","int":-12,"bool":true,"null":null}`, 0}, - {`{"values":{"string":"bar"}}`, `{"values":{"string":"bar"}}`, `{"string":"bar","int":42,"bool":true,"null":null}`, 0}, - {`{"values":{"int":-12}}`, `{"values":{"int":-12}}`, `{"string":"foo","int":-12,"bool":true,"null":null}`, 0}, - {`{"values":{"new":false}}`, `{"values":{"new":false}}`, `{"string":"foo","int":42,"bool":true,"null":null,"new":false}`, 0}, - {`{"values":{"int":{"action":"delete"}}}`, `{"values":{"int":{"action":"delete"}}}`, `{"string":"foo","bool":true,"null":null}`, 0}, + {"test.model", `{"values":{"string":"bar","int":-12}}`, `{"values":{"string":"bar","int":-12}}`, `{"string":"bar","int":-12,"bool":true,"null":null}`, 0}, + {"test.model", `{"values":{"string":"bar"}}`, `{"values":{"string":"bar"}}`, `{"string":"bar","int":42,"bool":true,"null":null}`, 0}, + {"test.model", `{"values":{"int":-12}}`, `{"values":{"int":-12}}`, `{"string":"foo","int":-12,"bool":true,"null":null}`, 0}, + {"test.model", `{"values":{"new":false}}`, `{"values":{"new":false}}`, `{"string":"foo","int":42,"bool":true,"null":null,"new":false}`, 0}, + {"test.model", `{"values":{"int":{"action":"delete"}}}`, `{"values":{"int":{"action":"delete"}}}`, `{"string":"foo","bool":true,"null":null}`, 0}, + {"test.model", `{"values":{"soft":{"rid":"test.model.soft","soft":true}}}`, `{"values":{"soft":{"rid":"test.model.soft","soft":true}}}`, `{"string":"foo","int":42,"bool":true,"null":null,"soft":{"rid":"test.model.soft","soft":true}}`, 0}, + {"test.model.soft", `{"values":{"child":null}}`, `{"values":{"child":null}}`, `{"name":"soft","child":null}`, 0}, + {"test.model.soft", `{"values":{"child":{"action":"delete"}}}`, `{"values":{"child":{"action":"delete"}}}`, `{"name":"soft"}`, 0}, // Unchanged values - {`{"values":{}}`, "", `{"string":"foo","int":42,"bool":true,"null":null}`, 0}, - {`{"values":{"string":"foo"}}`, "", `{"string":"foo","int":42,"bool":true,"null":null}`, 0}, - {`{"values":{"string":"foo","int":42}}`, "", `{"string":"foo","int":42,"bool":true,"null":null}`, 0}, - {`{"values":{"invalid":{"action":"delete"}}}`, "", `{"string":"foo","int":42,"bool":true,"null":null}`, 0}, - {`{"values":{"null":null,"string":"bar"}}`, `{"values":{"string":"bar"}}`, `{"string":"bar","int":42,"bool":true,"null":null}`, 0}, + {"test.model", `{"values":{}}`, "", `{"string":"foo","int":42,"bool":true,"null":null}`, 0}, + {"test.model", `{"values":{"string":"foo"}}`, "", `{"string":"foo","int":42,"bool":true,"null":null}`, 0}, + {"test.model", `{"values":{"string":"foo","int":42}}`, "", `{"string":"foo","int":42,"bool":true,"null":null}`, 0}, + {"test.model", `{"values":{"invalid":{"action":"delete"}}}`, "", `{"string":"foo","int":42,"bool":true,"null":null}`, 0}, + {"test.model", `{"values":{"null":null,"string":"bar"}}`, `{"values":{"string":"bar"}}`, `{"string":"bar","int":42,"bool":true,"null":null}`, 0}, + {"test.model.soft", `{"values":{"child":{"rid":"test.model","soft":true}}}`, "", `{"name":"soft","child":{"rid":"test.model","soft":true}}`, 0}, // Model change event v1.0 legacy behavior - {`{"string":"bar","int":-12}`, `{"values":{"string":"bar","int":-12}}`, `{"string":"bar","int":-12,"bool":true,"null":null}`, 1}, - {`{"string":"bar"}`, `{"values":{"string":"bar"}}`, `{"string":"bar","int":42,"bool":true,"null":null}`, 1}, + {"test.model", `{"string":"bar","int":-12}`, `{"values":{"string":"bar","int":-12}}`, `{"string":"bar","int":-12,"bool":true,"null":null}`, 1}, + {"test.model", `{"string":"bar"}`, `{"values":{"string":"bar"}}`, `{"string":"bar","int":42,"bool":true,"null":null}`, 1}, } for i, l := range tbl { @@ -75,31 +80,31 @@ func TestChangeEventOnCachedModel(t *testing.T) { var creq *ClientRequest c := s.Connect() - subscribeToTestModel(t, s, c) + subscribeToResource(t, s, c, l.RID) // Send event on model and validate client event - s.ResourceEvent("test.model", "change", json.RawMessage(l.ChangeEvent)) + s.ResourceEvent(l.RID, "change", json.RawMessage(l.ChangeEvent)) if l.ExpectedChangeEvent == "" { - c.AssertNoEvent(t, "test.model.change") + c.AssertNoEvent(t, l.RID+".change") } else { - c.GetEvent(t).Equals(t, "test.model.change", json.RawMessage(l.ExpectedChangeEvent)) + c.GetEvent(t).Equals(t, l.RID+".change", json.RawMessage(l.ExpectedChangeEvent)) } if sameClient { - c.Request("unsubscribe.test.model", nil).GetResponse(t) + c.Request("unsubscribe."+l.RID, nil).GetResponse(t) // Subscribe a second time - creq = c.Request("subscribe.test.model", nil) + creq = c.Request("subscribe."+l.RID, nil) } else { c2 := s.Connect() // Subscribe a second time - creq = c2.Request("subscribe.test.model", nil) + creq = c2.Request("subscribe."+l.RID, nil) } // Handle model access request - s.GetRequest(t).AssertSubject(t, "access.test.model").RespondSuccess(json.RawMessage(`{"get":true}`)) + s.GetRequest(t).AssertSubject(t, "access."+l.RID).RespondSuccess(json.RawMessage(`{"get":true}`)) // Validate client response - creq.GetResponse(t).AssertResult(t, json.RawMessage(`{"models":{"test.model":`+l.ExpectedModel+`}}`)) + creq.GetResponse(t).AssertResult(t, json.RawMessage(`{"models":{"`+l.RID+`":`+l.ExpectedModel+`}}`)) s.AssertErrorsLogged(t, l.ExpectedErrors) }) } @@ -179,3 +184,48 @@ func TestChangeEventWithChangedResourceReference(t *testing.T) { c.AssertNoEvent(t, "test.model") }) } + +// Test change event with removed resource reference +func TestChangeEvent_WithResourceReferenceReplacedBySoftReference_UnsubscribesReference(t *testing.T) { + runTest(t, func(s *Session) { + c := s.Connect() + subscribeToTestModelParent(t, s, c, false) + + // Send event on model and validate client event + s.ResourceEvent("test.model", "custom", common.CustomEvent()) + c.GetEvent(t).Equals(t, "test.model.custom", common.CustomEvent()) + + // Send event on model and validate client event + s.ResourceEvent("test.model.parent", "change", json.RawMessage(`{"values":{"child":{"rid":"test.model","soft":true}}}`)) + c.GetEvent(t).Equals(t, "test.model.parent.change", json.RawMessage(`{"values":{"child":{"rid":"test.model","soft":true}}}`)) + + // Send event on collection and validate client event is not sent to client + s.ResourceEvent("test.model", "custom", common.CustomEvent()) + c.AssertNoEvent(t, "test.model") + }) +} + +// Test change event with new resource reference +func TestChangeEvent_WithSoftReferenceReplacedByResourceReference_SubscribesReference(t *testing.T) { + model := resourceData("test.model") + + runTest(t, func(s *Session) { + c := s.Connect() + subscribeToResource(t, s, c, "test.model.soft") + + // Send event on model and validate client event + s.ResourceEvent("test.model.soft", "change", json.RawMessage(`{"values":{"child":{"rid":"test.model","soft":false}}}`)) + + // Handle model get request + s. + GetRequest(t). + AssertSubject(t, "get.test.model"). + RespondSuccess(json.RawMessage(`{"model":` + model + `}`)) + + c.GetEvent(t).Equals(t, "test.model.soft.change", json.RawMessage(`{"values":{"child":{"rid":"test.model","soft":false}},"models":{"test.model":`+model+`}}`)) + + // Send event on model and validate client event + s.ResourceEvent("test.model", "custom", common.CustomEvent()) + c.GetEvent(t).Equals(t, "test.model.custom", common.CustomEvent()) + }) +} diff --git a/test/08collection_event_test.go b/test/08collection_event_test.go index e0b4b04..5a62d23 100644 --- a/test/08collection_event_test.go +++ b/test/08collection_event_test.go @@ -25,16 +25,19 @@ func TestAddAndRemoveEventOnSubscribedResource(t *testing.T) { // Test add and remove event effects on cached collection func TestAddRemoveEventsOnCachedCollection(t *testing.T) { tbl := []struct { + RID string // Resource ID EventName string // Name of the event. Either add or remove. EventPayload string // Event payload (raw JSON) ExpectedCollection string // Expected collection after event (raw JSON) }{ - {"add", `{"idx":0,"value":"bar"}`, `["bar","foo",42,true,null]`}, - {"add", `{"idx":1,"value":"bar"}`, `["foo","bar",42,true,null]`}, - {"add", `{"idx":4,"value":"bar"}`, `["foo",42,true,null,"bar"]`}, - {"remove", `{"idx":0}`, `[42,true,null]`}, - {"remove", `{"idx":1}`, `["foo",true,null]`}, - {"remove", `{"idx":3}`, `["foo",42,true]`}, + {"test.collection", "add", `{"idx":0,"value":"bar"}`, `["bar","foo",42,true,null]`}, + {"test.collection", "add", `{"idx":1,"value":"bar"}`, `["foo","bar",42,true,null]`}, + {"test.collection", "add", `{"idx":4,"value":"bar"}`, `["foo",42,true,null,"bar"]`}, + {"test.collection", "add", `{"idx":0,"value":{"rid":"test.collection.soft","soft":true}}`, `[{"rid":"test.collection.soft","soft":true},"foo",42,true,null]`}, + {"test.collection", "remove", `{"idx":0}`, `[42,true,null]`}, + {"test.collection", "remove", `{"idx":1}`, `["foo",true,null]`}, + {"test.collection", "remove", `{"idx":3}`, `["foo",42,true]`}, + {"test.collection.soft", "remove", `{"idx":1}`, `["soft"]`}, } for i, l := range tbl { @@ -43,74 +46,28 @@ func TestAddRemoveEventsOnCachedCollection(t *testing.T) { var creq *ClientRequest c := s.Connect() - subscribeToTestCollection(t, s, c) + subscribeToResource(t, s, c, l.RID) // Send event on collection and validate client event - s.ResourceEvent("test.collection", l.EventName, json.RawMessage(l.EventPayload)) - c.GetEvent(t).Equals(t, "test.collection."+l.EventName, json.RawMessage(l.EventPayload)) + s.ResourceEvent(l.RID, l.EventName, json.RawMessage(l.EventPayload)) + c.GetEvent(t).Equals(t, l.RID+"."+l.EventName, json.RawMessage(l.EventPayload)) if sameClient { - c.Request("unsubscribe.test.collection", nil).GetResponse(t) + c.Request("unsubscribe."+l.RID, nil).GetResponse(t) // Subscribe a second time - creq = c.Request("subscribe.test.collection", nil) + creq = c.Request("subscribe."+l.RID, nil) } else { c2 := s.Connect() // Subscribe a second time - creq = c2.Request("subscribe.test.collection", nil) + creq = c2.Request("subscribe."+l.RID, nil) } // Handle collection access request - s.GetRequest(t).AssertSubject(t, "access.test.collection").RespondSuccess(json.RawMessage(`{"get":true}`)) + s.GetRequest(t).AssertSubject(t, "access."+l.RID).RespondSuccess(json.RawMessage(`{"get":true}`)) // Validate client response - creq.GetResponse(t).AssertResult(t, json.RawMessage(`{"collections":{"test.collection":`+l.ExpectedCollection+`}}`)) + creq.GetResponse(t).AssertResult(t, json.RawMessage(`{"collections":{"`+l.RID+`":`+l.ExpectedCollection+`}}`)) }) } } } - -// Test add event with new resource reference -func TestAddEventWithNewResourceReference(t *testing.T) { - model := resourceData("test.model") - - runTest(t, func(s *Session) { - - c := s.Connect() - subscribeToTestCollection(t, s, c) - - // Send event on collection and validate client event - s.ResourceEvent("test.collection", "add", json.RawMessage(`{"idx":1,"value":{"rid":"test.model"}}`)) - - // Handle collection get request - s. - GetRequest(t). - AssertSubject(t, "get.test.model"). - RespondSuccess(json.RawMessage(`{"model":` + model + `}`)) - - c.GetEvent(t).Equals(t, "test.collection.add", json.RawMessage(`{"idx":1,"value":{"rid":"test.model"},"models":{"test.model":`+model+`}}`)) - - // Send event on model and validate client event - s.ResourceEvent("test.model", "custom", common.CustomEvent()) - c.GetEvent(t).Equals(t, "test.model.custom", common.CustomEvent()) - }) -} - -// Test remove event with removed resource reference -func TestRemoveEventWithRemovedResourceReference(t *testing.T) { - runTest(t, func(s *Session) { - c := s.Connect() - subscribeToTestCollectionParent(t, s, c, false) - - // Send event on collection and validate client event - s.ResourceEvent("test.collection", "custom", common.CustomEvent()) - c.GetEvent(t).Equals(t, "test.collection.custom", common.CustomEvent()) - - // Send event on collection and validate client event - s.ResourceEvent("test.collection.parent", "remove", json.RawMessage(`{"idx":1}`)) - c.GetEvent(t).Equals(t, "test.collection.parent.remove", json.RawMessage(`{"idx":1}`)) - - // Send event on collection and validate client event is not sent to client - s.ResourceEvent("test.collection", "custom", common.CustomEvent()) - c.AssertNoEvent(t, "test.collection") - }) -} diff --git a/test/11system_event_test.go b/test/11system_event_test.go index d27259e..92ce7fb 100644 --- a/test/11system_event_test.go +++ b/test/11system_event_test.go @@ -58,43 +58,59 @@ func TestSystemResetTriggersGetRequestOnCollection(t *testing.T) { }) } -// Test that a system.reset event on modified model generates change event -func TestSystemResetGeneratesChangeEventOnModel(t *testing.T) { - runTest(t, func(s *Session) { - c := s.Connect() - - // Get model - subscribeToTestModel(t, s, c) - - // Send system reset - s.SystemEvent("reset", json.RawMessage(`{"resources":["test.>"]}`)) - - // Validate a get request is sent - s.GetRequest(t).AssertSubject(t, "get.test.model").RespondSuccess(json.RawMessage(`{"model":{"string":"bar","int":42,"bool":true}}`)) - - // Validate no events are sent to client - c.GetEvent(t).AssertEventName(t, "test.model.change").AssertData(t, json.RawMessage(`{"values":{"string":"bar","null":{"action":"delete"}}}`)) - }) -} +func TestSystemReset_WithUpdatedResource_GeneratesEvents(t *testing.T) { + type event struct { + Event string + Payload string + } + tbl := []struct { + RID string + ResetResponse string + ExpectedEvents []event + }{ + {"test.model", `{"model":{"string":"foo","int":42,"bool":true,"null":null}}`, []event{}}, + {"test.model", `{"model":{"string":"bar","int":42,"bool":true}}`, []event{ + {"change", `{"values":{"string":"bar","null":{"action":"delete"}}}`}, + }}, + {"test.model", `{"model":{"string":"foo","int":42,"bool":true,"null":null,"child":{"rid":"test.model","soft":true}}}`, []event{ + {"change", `{"values":{"child":{"rid":"test.model","soft":true}}}`}, + }}, + {"test.model.soft", `{"model":{"name":"soft","child":null}}`, []event{ + {"change", `{"values":{"child":null}}`}, + }}, + {"test.collection", `{"collection":["foo",42,true,null]}`, []event{}}, + {"test.collection", `{"collection":[42,"new",true,null]}`, []event{ + {"remove", `{"idx":0}`}, + {"add", `{"idx":1,"value":"new"}`}, + }}, + {"test.collection", `{"collection":["foo",42,true,null,{"rid":"test.model","soft":true}]}`, []event{ + {"add", `{"idx":4,"value":{"rid":"test.model","soft":true}}`}, + }}, + {"test.collection.soft", `{"collection":["soft"]}`, []event{ + {"remove", `{"idx":1}`}, + }}, + } -// Test that a system.reset event on modified collection generates add and remove events -func TestSystemResetGeneratesAddRemoveEventsOnCollection(t *testing.T) { - runTest(t, func(s *Session) { - c := s.Connect() + for i, l := range tbl { + runNamedTest(t, fmt.Sprintf("#%d", i+1), func(s *Session) { + c := s.Connect() - // Get collection - subscribeToTestCollection(t, s, c) + // Get collection + subscribeToResource(t, s, c, l.RID) - // Send system reset - s.SystemEvent("reset", json.RawMessage(`{"resources":["test.>"]}`)) + // Send system reset + s.SystemEvent("reset", json.RawMessage(`{"resources":["test.>"]}`)) - // Validate a get request is sent - s.GetRequest(t).AssertSubject(t, "get.test.collection").RespondSuccess(json.RawMessage(`{"collection":[42,"new",true,null]}`)) + // Validate a get request is sent + s.GetRequest(t).AssertSubject(t, "get."+l.RID).RespondSuccess(json.RawMessage(l.ResetResponse)) - // Validate no events are sent to client - c.GetEvent(t).AssertEventName(t, "test.collection.remove").AssertData(t, json.RawMessage(`{"idx":0}`)) - c.GetEvent(t).AssertEventName(t, "test.collection.add").AssertData(t, json.RawMessage(`{"idx":1,"value":"new"}`)) - }) + for _, ev := range l.ExpectedEvents { + // Validate no events are sent to client + c.GetEvent(t).AssertEventName(t, l.RID+"."+ev.Event).AssertData(t, json.RawMessage(ev.Payload)) + } + c.AssertNoEvent(t, l.RID) + }) + } } // Test that a system.reset event triggers a re-access call on subscribed resources @@ -281,3 +297,41 @@ func TestSystemReset_InternalErrorResponseOnCollection_LogsError(t *testing.T) { s.AssertErrorsLogged(t, 1) }) } + +func TestSystemReset_MismatchingResourceTypeResponseOnModel_LogsError(t *testing.T) { + runTest(t, func(s *Session) { + c := s.Connect() + // Get model + subscribeToTestModel(t, s, c) + // Send system reset + s.SystemEvent("reset", json.RawMessage(`{"resources":["test.>"]}`)) + // Respond to get request with mismatching type + s.GetRequest(t).AssertSubject(t, "get.test.model").RespondSuccess(json.RawMessage(`{"collection":["foo",42,true,null]}`)) + // Validate no delete event is sent to client + c.AssertNoEvent(t, "test.model") + // Validate subsequent events are sent to client + s.ResourceEvent("test.model", "custom", common.CustomEvent()) + c.GetEvent(t).Equals(t, "test.model.custom", common.CustomEvent()) + // Assert error is logged + s.AssertErrorsLogged(t, 1) + }) +} + +func TestSystemReset_MismatchingResourceTypeResponseOnCollection_LogsError(t *testing.T) { + runTest(t, func(s *Session) { + c := s.Connect() + // Get collection + subscribeToTestCollection(t, s, c) + // Send system reset + s.SystemEvent("reset", json.RawMessage(`{"resources":["test.>"]}`)) + // Respond to get request with mismatching type + s.GetRequest(t).AssertSubject(t, "get.test.collection").RespondSuccess(json.RawMessage(`{"model":{"string":"foo","int":42,"bool":true,"null":null}}`)) + // Validate no delete event is sent to client + c.AssertNoEvent(t, "test.collection") + // Validate subsequent events are sent to client + s.ResourceEvent("test.collection", "custom", common.CustomEvent()) + c.GetEvent(t).Equals(t, "test.collection.custom", common.CustomEvent()) + // Assert error is logged + s.AssertErrorsLogged(t, 1) + }) +} diff --git a/test/14http_get_test.go b/test/14http_get_test.go index d9973b1..6508374 100644 --- a/test/14http_get_test.go +++ b/test/14http_get_test.go @@ -61,6 +61,8 @@ func TestHTTPGet(t *testing.T) { "test.model.grandparent": `{"name":"grandparent","child":{"href":"/api/test/model/parent","model":{"name":"parent","child":{"href":"/api/test/model","model":` + resourceData("test.model") + `}}}}`, "test.model.secondparent": `{"name":"secondparent","child":{"href":"/api/test/model","model":` + resourceData("test.model") + `}}`, "test.model.brokenchild": `{"name":"brokenchild","child":{"href":"/api/test/err/notFound","error":` + resourceData("test.err.notFound") + `}}`, + "test.model.soft": `{"name":"soft","child":{"href":"/api/test/model"}}`, + "test.model.softparent": `{"name":"parent","child":{"href":"/api/test/model/soft","model":{"name":"soft","child":{"href":"/api/test/model"}}}}`, "test.m.a": `{"a":{"href":"/api/test/m/a"}}`, "test.m.b": `{"c":{"href":"/api/test/m/c","model":{"b":{"href":"/api/test/m/b"}}}}`, "test.m.d": `{"e":{"href":"/api/test/m/e","model":{"d":{"href":"/api/test/m/d"}}},"f":{"href":"/api/test/m/f","model":{"d":{"href":"/api/test/m/d"}}}}`, @@ -72,6 +74,8 @@ func TestHTTPGet(t *testing.T) { "test.collection.grandparent": `["grandparent",{"href":"/api/test/collection/parent","collection":["parent",{"href":"/api/test/collection","collection":` + resourceData("test.collection") + `}]}]`, "test.collection.secondparent": `["secondparent",{"href":"/api/test/collection","collection":` + resourceData("test.collection") + `}]`, "test.collection.brokenchild": `["brokenchild",{"href":"/api/test/err/notFound","error":` + resourceData("test.err.notFound") + `}]`, + "test.collection.soft": `["soft",{"href":"/api/test/collection"}]`, + "test.collection.soft.parent": `["parent",{"href":"/api/test/collection","collection":["soft",{"href":"/api/test/collection"}]}]`, "test.c.a": `[{"href":"/api/test/c/a"}]`, "test.c.b": `[{"href":"/api/test/c/c","collection":[{"href":"/api/test/c/b"}]}]`, "test.c.d": `[{"href":"/api/test/c/e","collection":[{"href":"/api/test/c/d"}]},{"href":"/api/test/c/f","collection":[{"href":"/api/test/c/d"}]}]`, @@ -88,6 +92,8 @@ func TestHTTPGet(t *testing.T) { "test.model.grandparent": `{"name":"grandparent","child":{"name":"parent","child":` + resourceData("test.model") + `}}`, "test.model.secondparent": `{"name":"secondparent","child":` + resourceData("test.model") + `}`, "test.model.brokenchild": `{"name":"brokenchild","child":` + resourceData("test.err.notFound") + `}`, + "test.model.soft": `{"name":"soft","child":{"href":"/api/test/model"}}`, + "test.model.soft.parent": `{"name":"softparent","child":{"name":"soft","child":{"href":"/api/test/model"}}}`, "test.m.a": `{"a":{"href":"/api/test/m/a"}}`, "test.m.b": `{"c":{"b":{"href":"/api/test/m/b"}}}`, "test.m.d": `{"e":{"d":{"href":"/api/test/m/d"}},"f":{"d":{"href":"/api/test/m/d"}}}`, @@ -99,6 +105,8 @@ func TestHTTPGet(t *testing.T) { "test.collection.grandparent": `["grandparent",["parent",` + resourceData("test.collection") + `]]`, "test.collection.secondparent": `["secondparent",` + resourceData("test.collection") + `]`, "test.collection.brokenchild": `["brokenchild",` + resourceData("test.err.notFound") + `]`, + "test.collection.soft": `["soft",{"href":"/api/test/collection"}]`, + "test.collection.soft.parent": `["softparent",{"name":"soft","child":{"href":"/api/test/collection"}}]`, "test.c.a": `[{"href":"/api/test/c/a"}]`, "test.c.b": `[[{"href":"/api/test/c/b"}]]`, "test.c.d": `[[{"href":"/api/test/c/d"}],[{"href":"/api/test/c/d"}]]`, @@ -109,56 +117,61 @@ func TestHTTPGet(t *testing.T) { } for _, enc := range encodings { - for i, l := range sequenceTable { - runNamedTest(t, fmt.Sprintf("#%d with APIEncoding %#v", i+1, enc.APIEncoding), func(s *Session) { - var hreq *HTTPRequest - var req *Request + for _, set := range sequenceSets { + if set.Version != versionLatest { + continue + } + for i, l := range set.Table { + runNamedTest(t, fmt.Sprintf("#%d with APIEncoding %#v", i+1, enc.APIEncoding), func(s *Session) { + var hreq *HTTPRequest + var req *Request - hreqs := make(map[string]*HTTPRequest) - reqs := make(map[string]*Request) + hreqs := make(map[string]*HTTPRequest) + reqs := make(map[string]*Request) - for _, ev := range l { - switch ev.Event { - case "subscribe": - url := "/api/" + strings.Replace(ev.RID, ".", "/", -1) - hreqs[ev.RID] = s.HTTPRequest("GET", url, nil) - case "access": - for req = reqs["access."+ev.RID]; req == nil; req = reqs["access."+ev.RID] { - treq := s.GetRequest(t) - reqs[treq.Subject] = treq - } - req.RespondSuccess(json.RawMessage(`{"get":true}`)) - case "accessDenied": - for req = reqs["access."+ev.RID]; req == nil; req = reqs["access."+ev.RID] { - treq := s.GetRequest(t) - reqs[treq.Subject] = treq - } - req.RespondSuccess(json.RawMessage(`{"get":false}`)) - case "get": - for req = reqs["get."+ev.RID]; req == nil; req = reqs["get."+ev.RID] { - req = s.GetRequest(t) - reqs[req.Subject] = req - } - rsrc := resources[ev.RID] - switch rsrc.typ { - case typeModel: - req.RespondSuccess(json.RawMessage(`{"model":` + rsrc.data + `}`)) - case typeCollection: - req.RespondSuccess(json.RawMessage(`{"collection":` + rsrc.data + `}`)) - case typeError: - req.RespondError(rsrc.err) + for _, ev := range l { + switch ev.Event { + case "subscribe": + url := "/api/" + strings.Replace(ev.RID, ".", "/", -1) + hreqs[ev.RID] = s.HTTPRequest("GET", url, nil) + case "access": + for req = reqs["access."+ev.RID]; req == nil; req = reqs["access."+ev.RID] { + treq := s.GetRequest(t) + reqs[treq.Subject] = treq + } + req.RespondSuccess(json.RawMessage(`{"get":true}`)) + case "accessDenied": + for req = reqs["access."+ev.RID]; req == nil; req = reqs["access."+ev.RID] { + treq := s.GetRequest(t) + reqs[treq.Subject] = treq + } + req.RespondSuccess(json.RawMessage(`{"get":false}`)) + case "get": + for req = reqs["get."+ev.RID]; req == nil; req = reqs["get."+ev.RID] { + req = s.GetRequest(t) + reqs[req.Subject] = req + } + rsrc := resources[ev.RID] + switch rsrc.typ { + case typeModel: + req.RespondSuccess(json.RawMessage(`{"model":` + rsrc.data + `}`)) + case typeCollection: + req.RespondSuccess(json.RawMessage(`{"collection":` + rsrc.data + `}`)) + case typeError: + req.RespondError(rsrc.err) + } + case "response": + hreq = hreqs[ev.RID] + hreq.GetResponse(t).Equals(t, http.StatusOK, json.RawMessage(enc.Responses[ev.RID])) + case "errorResponse": + hreq = hreqs[ev.RID] + hreq.GetResponse(t).AssertIsError(t) } - case "response": - hreq = hreqs[ev.RID] - hreq.GetResponse(t).Equals(t, http.StatusOK, json.RawMessage(enc.Responses[ev.RID])) - case "errorResponse": - hreq = hreqs[ev.RID] - hreq.GetResponse(t).AssertIsError(t) } - } - }, func(c *server.Config) { - c.APIEncoding = enc.APIEncoding - }) + }, func(c *server.Config) { + c.APIEncoding = enc.APIEncoding + }) + } } } } diff --git a/test/20version_test.go b/test/20version_test.go index 5e747fb..cbc2ba5 100644 --- a/test/20version_test.go +++ b/test/20version_test.go @@ -5,14 +5,11 @@ import ( "fmt" "testing" - "github.com/resgateio/resgate/server" "github.com/resgateio/resgate/server/reserr" ) func TestVersion_Request_ReturnsExpectedResponse(t *testing.T) { - versionResult := json.RawMessage(fmt.Sprintf(`{"protocol":"%s"}`, server.ProtocolVersion)) - tbl := []struct { Params json.RawMessage Expected interface{} diff --git a/test/40legacy_1.2.0_test.go b/test/40legacy_1.2.0_test.go new file mode 100644 index 0000000..98a9813 --- /dev/null +++ b/test/40legacy_1.2.0_test.go @@ -0,0 +1,162 @@ +package test + +import ( + "encoding/json" + "fmt" + "testing" +) + +// Test change event effect on cached model +func TestLegacy120ChangeEvent_OnCachedModel(t *testing.T) { + tbl := []struct { + RID string // RID of resource to subscribe to + ChangeEvent string // Change event to send (raw JSON) + ExpectedChangeEvent string // Expected event sent to client (raw JSON. Empty means none) + ExpectedModel string // Expected model after event (raw JSON) + ExpectedErrors int + }{ + {"test.model", `{"values":{"soft":{"rid":"test.model.soft","soft":true}}}`, `{"values":{"soft":"test.model.soft"}}`, `{"string":"foo","int":42,"bool":true,"null":null,"soft":"test.model.soft"}`, 0}, + {"test.model.soft", `{"values":{"child":null}}`, `{"values":{"child":null}}`, `{"name":"soft","child":null}`, 0}, + {"test.model.soft", `{"values":{"child":{"action":"delete"}}}`, `{"values":{"child":{"action":"delete"}}}`, `{"name":"soft"}`, 0}, + + // Unchanged values + {"test.model.soft", `{"values":{"child":{"rid":"test.model","soft":true}}}`, "", `{"name":"soft","child":"test.model"}`, 0}, + } + + for i, l := range tbl { + for sameClient := true; sameClient; sameClient = false { + runNamedTest(t, fmt.Sprintf("#%d with the same client being %+v", i+1, sameClient), func(s *Session) { + var creq *ClientRequest + + rid := l.RID + c := s.ConnectWithVersion("1.2.0") + + // Subscribe to resource + r := resources[rid].data + // Send subscribe request + creq = c.Request("subscribe."+rid, nil) + // Handle model get and access request + mreqs := s.GetParallelRequests(t, 2) + req := mreqs.GetRequest(t, "access."+rid) + req.RespondSuccess(json.RawMessage(`{"get":true}`)) + mreqs.GetRequest(t, "get."+rid).RespondSuccess(json.RawMessage(`{"model":` + r + `}`)) + creq.GetResponse(t) + + // Send event on model and validate client event + s.ResourceEvent(rid, "change", json.RawMessage(l.ChangeEvent)) + if l.ExpectedChangeEvent == "" { + c.AssertNoEvent(t, rid+".change") + } else { + c.GetEvent(t).Equals(t, rid+".change", json.RawMessage(l.ExpectedChangeEvent)) + } + + if sameClient { + c.Request("unsubscribe."+rid, nil).GetResponse(t) + // Subscribe a second time + creq = c.Request("subscribe."+rid, nil) + } else { + c2 := s.Connect() + // Subscribe a second time + creq = c2.Request("subscribe."+rid, nil) + } + + // Handle model access request + s.GetRequest(t).AssertSubject(t, "access."+rid).RespondSuccess(json.RawMessage(`{"get":true}`)) + + // Validate client response + creq.GetResponse(t).AssertResult(t, json.RawMessage(`{"models":{"`+rid+`":`+l.ExpectedModel+`}}`)) + s.AssertErrorsLogged(t, l.ExpectedErrors) + }) + } + } +} + +// Test change event with new resource reference +func TestLegacy120ChangeEvent_WithSoftReferenceReplacedByResourceReference_SubscribesReference(t *testing.T) { + model := resourceData("test.model") + + runTest(t, func(s *Session) { + c := s.ConnectWithVersion("1.2.0") + // Subscribe to resource + rid := "test.model.soft" + r := resources[rid].data + // Send subscribe request + creq := c.Request("subscribe."+rid, nil) + // Handle model get and access request + mreqs := s.GetParallelRequests(t, 2) + req := mreqs.GetRequest(t, "access."+rid) + req.RespondSuccess(json.RawMessage(`{"get":true}`)) + mreqs.GetRequest(t, "get."+rid).RespondSuccess(json.RawMessage(`{"model":` + r + `}`)) + creq.GetResponse(t) + + // Send event on model and validate client event + s.ResourceEvent(rid, "change", json.RawMessage(`{"values":{"child":{"rid":"test.model","soft":false}}}`)) + + // Handle model get request + s. + GetRequest(t). + AssertSubject(t, "get.test.model"). + RespondSuccess(json.RawMessage(`{"model":` + model + `}`)) + + c.GetEvent(t).Equals(t, rid+".change", json.RawMessage(`{"values":{"child":{"rid":"test.model","soft":false}},"models":{"test.model":`+model+`}}`)) + + // Send event on model and validate client event + s.ResourceEvent("test.model", "custom", common.CustomEvent()) + c.GetEvent(t).Equals(t, "test.model.custom", common.CustomEvent()) + }) +} + +// Test add and remove event effects on cached collection +func TestLegacy120AddRemoveEvents_OnCachedCollection(t *testing.T) { + tbl := []struct { + RID string // Resource ID + EventName string // Name of the event. Either add or remove. + EventPayload string // Event payload (raw JSON) + ClientEventPayload string // Event payload as sent to client(raw JSON) + ExpectedCollection string // Expected collection after event (raw JSON) + }{ + {"test.collection", "add", `{"idx":0,"value":{"rid":"test.collection.soft","soft":true}}`, `{"idx":0,"value":"test.collection.soft"}`, `["test.collection.soft","foo",42,true,null]`}, + {"test.collection.soft", "remove", `{"idx":1}`, `{"idx":1}`, `["soft"]`}, + } + + for i, l := range tbl { + for sameClient := true; sameClient; sameClient = false { + runNamedTest(t, fmt.Sprintf("#%d with the same client being %+v", i+1, sameClient), func(s *Session) { + var creq *ClientRequest + + c := s.ConnectWithVersion("1.2.0") + // Subscribe to resource + rid := l.RID + r := resources[rid].data + // Send subscribe request + creq = c.Request("subscribe."+rid, nil) + // Handle model get and access request + mreqs := s.GetParallelRequests(t, 2) + req := mreqs.GetRequest(t, "access."+rid) + req.RespondSuccess(json.RawMessage(`{"get":true}`)) + mreqs.GetRequest(t, "get."+rid).RespondSuccess(json.RawMessage(`{"collection":` + r + `}`)) + creq.GetResponse(t) + + // Send event on collection and validate client event + s.ResourceEvent(rid, l.EventName, json.RawMessage(l.EventPayload)) + c.GetEvent(t).Equals(t, rid+"."+l.EventName, json.RawMessage(l.ClientEventPayload)) + + if sameClient { + c.Request("unsubscribe."+rid, nil).GetResponse(t) + // Subscribe a second time + creq = c.Request("subscribe."+rid, nil) + } else { + c2 := s.Connect() + // Subscribe a second time + creq = c2.Request("subscribe."+rid, nil) + } + + // Handle collection access request + s.GetRequest(t).AssertSubject(t, "access."+l.RID).RespondSuccess(json.RawMessage(`{"get":true}`)) + + // Validate client response + creq.GetResponse(t).AssertResult(t, json.RawMessage(`{"collections":{"`+l.RID+`":`+l.ExpectedCollection+`}}`)) + }) + } + } +} diff --git a/test/common.go b/test/common.go index 034002e..4e7afb6 100644 --- a/test/common.go +++ b/test/common.go @@ -14,20 +14,42 @@ func (c *commonData) CustomEvent() json.RawMessage { return json.RawMessage(`{"f // subscribeToTestModel makes a successful subscription to test.model // Returns the connection ID (cid) func subscribeToTestModel(t *testing.T, s *Session, c *Conn) string { - model := resourceData("test.model") + return subscribeToResource(t, s, c, "test.model") +} + +func subscribeToResource(t *testing.T, s *Session, c *Conn, rid string) string { + rsrc, ok := resources[rid] + if !ok { + panic("no resource named " + rid) + } + var r string + if rsrc.typ == typeError { + b, _ := json.Marshal(rsrc.err) + r = string(b) + } else { + r = rsrc.data + } // Send subscribe request - creq := c.Request("subscribe.test.model", nil) + creq := c.Request("subscribe."+rid, nil) // Handle model get and access request mreqs := s.GetParallelRequests(t, 2) - mreqs.GetRequest(t, "get.test.model").RespondSuccess(json.RawMessage(`{"model":` + model + `}`)) - req := mreqs.GetRequest(t, "access.test.model") + // Handle access + req := mreqs.GetRequest(t, "access."+rid) cid := req.PathPayload(t, "cid").(string) req.RespondSuccess(json.RawMessage(`{"get":true}`)) - - // Validate client response and validate - creq.GetResponse(t).AssertResult(t, json.RawMessage(`{"models":{"test.model":`+model+`}}`)) + // Handle resource and validate client response + switch rsrc.typ { + case typeModel: + mreqs.GetRequest(t, "get."+rid).RespondSuccess(json.RawMessage(`{"model":` + r + `}`)) + creq.GetResponse(t).AssertResult(t, json.RawMessage(`{"models":{"`+rid+`":`+r+`}}`)) + case typeCollection: + mreqs.GetRequest(t, "get."+rid).RespondSuccess(json.RawMessage(`{"collection":` + r + `}`)) + creq.GetResponse(t).AssertResult(t, json.RawMessage(`{"collections":{"`+rid+`":`+r+`}}`)) + default: + panic("invalid type") + } return cid } @@ -82,22 +104,7 @@ func subscribeToTestModelParentExt(t *testing.T, s *Session, c *Conn, childIsSub // subscribeToTestCollection makes a successful subscription to test.collection // Returns the connection ID (cid) of the access request func subscribeToTestCollection(t *testing.T, s *Session, c *Conn) string { - collection := resourceData("test.collection") - - // Send subscribe request - creq := c.Request("subscribe.test.collection", nil) - - // Handle collection get and access request - mreqs := s.GetParallelRequests(t, 2) - mreqs.GetRequest(t, "get.test.collection").RespondSuccess(json.RawMessage(`{"collection":` + collection + `}`)) - req := mreqs.GetRequest(t, "access.test.collection") - cid := req.PathPayload(t, "cid").(string) - req.RespondSuccess(json.RawMessage(`{"get":true}`)) - - // Validate client response and validate - creq.GetResponse(t).AssertResult(t, json.RawMessage(`{"collections":{"test.collection":`+collection+`}}`)) - - return cid + return subscribeToResource(t, s, c, "test.collection") } // subscribeToTestCollectionParent makes a successful subscription to test.collection.parent diff --git a/test/natstest.go b/test/natstest.go index 8ae5021..760e225 100644 --- a/test/natstest.go +++ b/test/natstest.go @@ -182,6 +182,19 @@ func (c *NATSTestClient) HasSubscriptions(t *testing.T, rids ...string) { } } +// NoSubscriptions asserts that there isn't any subscription for the given +// resource IDs. +func (c *NATSTestClient) NoSubscriptions(t *testing.T, rids ...string) { + c.mu.Lock() + defer c.mu.Unlock() + + for _, rid := range rids { + if _, ok := c.subs["event."+rid]; ok { + t.Fatalf("expected no subscription for event.%s.*, but found one", rid) + } + } +} + // ResourceEvent sends a resource event to resgate. The subject will be "event."+rid+"."+event . // It panics if there is no subscription for such event. func (c *NATSTestClient) ResourceEvent(rid string, event string, payload interface{}) { diff --git a/test/resources.go b/test/resources.go index 734405e..3a5b060 100644 --- a/test/resources.go +++ b/test/resources.go @@ -34,7 +34,10 @@ type resource struct { } func resourceData(rid string) string { - rsrc := resources[rid] + rsrc, ok := resources[rid] + if !ok { + panic("no resource named " + rid) + } if rsrc.typ == typeError { b, _ := json.Marshal(rsrc.err) return string(b) @@ -49,6 +52,8 @@ var resources = map[string]resource{ "test.model.secondparent": {typeModel, `{"name":"secondparent","child":{"rid":"test.model"}}`, nil}, "test.model.grandparent": {typeModel, `{"name":"grandparent","child":{"rid":"test.model.parent"}}`, nil}, "test.model.brokenchild": {typeModel, `{"name":"brokenchild","child":{"rid":"test.err.notFound"}}`, nil}, + "test.model.soft": {typeModel, `{"name":"soft","child":{"rid":"test.model","soft":true}}`, nil}, + "test.model.soft.parent": {typeModel, `{"name":"softparent","child":{"rid":"test.model.soft","soft":false}}`, nil}, // Cyclic model resources "test.m.a": {typeModel, `{"a":{"rid":"test.m.a"}}`, nil}, @@ -69,6 +74,8 @@ var resources = map[string]resource{ "test.collection.secondparent": {typeCollection, `["secondparent",{"rid":"test.collection"}]`, nil}, "test.collection.grandparent": {typeCollection, `["grandparent",{"rid":"test.collection.parent"}]`, nil}, "test.collection.brokenchild": {typeCollection, `["brokenchild",{"rid":"test.err.notFound"}]`, nil}, + "test.collection.soft": {typeCollection, `["soft",{"rid":"test.collection","soft":true}]`, nil}, + "test.collection.soft.parent": {typeCollection, `["softparent",{"rid":"test.collection.soft","soft":false}]`, nil}, // Cyclic collection resources "test.c.a": {typeCollection, `[{"rid":"test.c.a"}]`, nil}, @@ -100,213 +107,262 @@ type sequenceEvent struct { RID string } -var sequenceTable = [][]sequenceEvent{ - // Model tests - { - {"subscribe", "test.model"}, - {"access", "test.model"}, - {"get", "test.model"}, - {"response", "test.model"}, - {"event", "test.model"}, - }, - { - {"subscribe", "test.model.parent"}, - {"access", "test.model.parent"}, - {"get", "test.model.parent"}, - {"get", "test.model"}, - {"response", "test.model.parent"}, - {"event", "test.model.parent"}, - {"event", "test.model"}, - }, - { - {"subscribe", "test.model.grandparent"}, - {"access", "test.model.grandparent"}, - {"get", "test.model.grandparent"}, - {"get", "test.model.parent"}, - {"get", "test.model"}, - {"response", "test.model.grandparent"}, - {"event", "test.model.grandparent"}, - {"event", "test.model.parent"}, - {"event", "test.model"}, - }, - { - {"subscribe", "test.model.parent"}, - {"access", "test.model.parent"}, - {"get", "test.model.parent"}, - {"get", "test.model"}, - {"response", "test.model.parent"}, - {"subscribe", "test.model.secondparent"}, - {"access", "test.model.secondparent"}, - {"get", "test.model.secondparent"}, - {"response", "test.model.secondparent"}, - }, - { - {"subscribe", "test.model.brokenchild"}, - {"access", "test.model.brokenchild"}, - {"get", "test.model.brokenchild"}, - {"get", "test.err.notFound"}, - {"response", "test.model.brokenchild"}, - {"event", "test.model.brokenchild"}, - {"noevent", "test.err.notFound"}, - }, - // Cyclic model tests - { - {"subscribe", "test.m.a"}, - {"access", "test.m.a"}, - {"get", "test.m.a"}, - {"response", "test.m.a"}, - }, - { - {"subscribe", "test.m.b"}, - {"access", "test.m.b"}, - {"get", "test.m.b"}, - {"get", "test.m.c"}, - {"response", "test.m.b"}, - }, - { - {"subscribe", "test.m.d"}, - {"access", "test.m.d"}, - {"get", "test.m.d"}, - {"get", "test.m.e"}, - {"get", "test.m.f"}, - {"response", "test.m.d"}, - }, - { - {"subscribe", "test.m.g"}, - {"access", "test.m.g"}, - {"get", "test.m.g"}, - {"get", "test.m.e"}, - {"get", "test.m.f"}, - {"get", "test.m.d"}, - {"response", "test.m.g"}, - }, - { - {"subscribe", "test.m.d"}, - {"access", "test.m.d"}, - {"get", "test.m.d"}, - {"subscribe", "test.m.h"}, - {"access", "test.m.h"}, - {"get", "test.m.e"}, - {"get", "test.m.h"}, - {"get", "test.m.f"}, - {"response", "test.m.d"}, - {"response", "test.m.h"}, - }, +type sequenceSet struct { + Version string + Table [][]sequenceEvent +} - // Collection tests +var sequenceSets = []sequenceSet{ { - {"subscribe", "test.collection"}, - {"access", "test.collection"}, - {"get", "test.collection"}, - {"response", "test.collection"}, - {"event", "test.collection"}, - }, - { - {"subscribe", "test.collection.parent"}, - {"access", "test.collection.parent"}, - {"get", "test.collection.parent"}, - {"get", "test.collection"}, - {"response", "test.collection.parent"}, - {"event", "test.collection.parent"}, - {"event", "test.collection"}, - }, - { - {"subscribe", "test.collection.grandparent"}, - {"access", "test.collection.grandparent"}, - {"get", "test.collection.grandparent"}, - {"get", "test.collection.parent"}, - {"get", "test.collection"}, - {"response", "test.collection.grandparent"}, - {"event", "test.collection.grandparent"}, - {"event", "test.collection.parent"}, - {"event", "test.collection"}, - }, - { - {"subscribe", "test.collection.parent"}, - {"access", "test.collection.parent"}, - {"get", "test.collection.parent"}, - {"get", "test.collection"}, - {"response", "test.collection.parent"}, - {"subscribe", "test.collection.secondparent"}, - {"access", "test.collection.secondparent"}, - {"get", "test.collection.secondparent"}, - {"response", "test.collection.secondparent"}, - }, - { - {"subscribe", "test.collection.brokenchild"}, - {"access", "test.collection.brokenchild"}, - {"get", "test.collection.brokenchild"}, - {"get", "test.err.notFound"}, - {"response", "test.collection.brokenchild"}, - {"event", "test.collection.brokenchild"}, - {"noevent", "test.err.notFound"}, - }, - // Cyclic collection tests - { - {"subscribe", "test.c.a"}, - {"access", "test.c.a"}, - {"get", "test.c.a"}, - {"response", "test.c.a"}, - }, - { - {"subscribe", "test.c.b"}, - {"access", "test.c.b"}, - {"get", "test.c.b"}, - {"get", "test.c.c"}, - {"response", "test.c.b"}, - }, - { - {"subscribe", "test.c.d"}, - {"access", "test.c.d"}, - {"get", "test.c.d"}, - {"get", "test.c.e"}, - {"get", "test.c.f"}, - {"response", "test.c.d"}, - }, - { - {"subscribe", "test.c.g"}, - {"access", "test.c.g"}, - {"get", "test.c.g"}, - {"get", "test.c.e"}, - {"get", "test.c.f"}, - {"get", "test.c.d"}, - {"response", "test.c.g"}, - }, - { - {"subscribe", "test.c.d"}, - {"access", "test.c.d"}, - {"get", "test.c.d"}, - {"subscribe", "test.c.h"}, - {"access", "test.c.h"}, - {"get", "test.c.e"}, - {"get", "test.c.h"}, - {"get", "test.c.f"}, - {"response", "test.c.d"}, - {"response", "test.c.h"}, - }, - // Access test - { - {"subscribe", "test.model.parent"}, - {"access", "test.model.parent"}, - {"get", "test.model.parent"}, - {"get", "test.model"}, - {"response", "test.model.parent"}, - {"subscribe", "test.model"}, - {"access", "test.model"}, - {"response", "test.model"}, - {"event", "test.model.parent"}, - {"event", "test.model"}, + versionLatest, + [][]sequenceEvent{ + // Model tests + { + {"subscribe", "test.model"}, + {"access", "test.model"}, + {"get", "test.model"}, + {"response", "test.model"}, + {"event", "test.model"}, + }, + { + {"subscribe", "test.model.parent"}, + {"access", "test.model.parent"}, + {"get", "test.model.parent"}, + {"get", "test.model"}, + {"response", "test.model.parent"}, + {"event", "test.model.parent"}, + {"event", "test.model"}, + }, + { + {"subscribe", "test.model.grandparent"}, + {"access", "test.model.grandparent"}, + {"get", "test.model.grandparent"}, + {"get", "test.model.parent"}, + {"get", "test.model"}, + {"response", "test.model.grandparent"}, + {"event", "test.model.grandparent"}, + {"event", "test.model.parent"}, + {"event", "test.model"}, + }, + { + {"subscribe", "test.model.parent"}, + {"access", "test.model.parent"}, + {"get", "test.model.parent"}, + {"get", "test.model"}, + {"response", "test.model.parent"}, + {"subscribe", "test.model.secondparent"}, + {"access", "test.model.secondparent"}, + {"get", "test.model.secondparent"}, + {"response", "test.model.secondparent"}, + }, + { + {"subscribe", "test.model.brokenchild"}, + {"access", "test.model.brokenchild"}, + {"get", "test.model.brokenchild"}, + {"get", "test.err.notFound"}, + {"response", "test.model.brokenchild"}, + {"event", "test.model.brokenchild"}, + {"noevent", "test.err.notFound"}, + }, + { + {"subscribe", "test.model.soft"}, + {"access", "test.model.soft"}, + {"get", "test.model.soft"}, + {"response", "test.model.soft"}, + {"event", "test.model.soft"}, + {"nosubscription", "test.model"}, + }, + // Cyclic model tests + { + {"subscribe", "test.m.a"}, + {"access", "test.m.a"}, + {"get", "test.m.a"}, + {"response", "test.m.a"}, + }, + { + {"subscribe", "test.m.b"}, + {"access", "test.m.b"}, + {"get", "test.m.b"}, + {"get", "test.m.c"}, + {"response", "test.m.b"}, + }, + { + {"subscribe", "test.m.d"}, + {"access", "test.m.d"}, + {"get", "test.m.d"}, + {"get", "test.m.e"}, + {"get", "test.m.f"}, + {"response", "test.m.d"}, + }, + { + {"subscribe", "test.m.g"}, + {"access", "test.m.g"}, + {"get", "test.m.g"}, + {"get", "test.m.e"}, + {"get", "test.m.f"}, + {"get", "test.m.d"}, + {"response", "test.m.g"}, + }, + { + {"subscribe", "test.m.d"}, + {"access", "test.m.d"}, + {"get", "test.m.d"}, + {"subscribe", "test.m.h"}, + {"access", "test.m.h"}, + {"get", "test.m.e"}, + {"get", "test.m.h"}, + {"get", "test.m.f"}, + {"response", "test.m.d"}, + {"response", "test.m.h"}, + }, + + // Collection tests + { + {"subscribe", "test.collection"}, + {"access", "test.collection"}, + {"get", "test.collection"}, + {"response", "test.collection"}, + {"event", "test.collection"}, + }, + { + {"subscribe", "test.collection.parent"}, + {"access", "test.collection.parent"}, + {"get", "test.collection.parent"}, + {"get", "test.collection"}, + {"response", "test.collection.parent"}, + {"event", "test.collection.parent"}, + {"event", "test.collection"}, + }, + { + {"subscribe", "test.collection.grandparent"}, + {"access", "test.collection.grandparent"}, + {"get", "test.collection.grandparent"}, + {"get", "test.collection.parent"}, + {"get", "test.collection"}, + {"response", "test.collection.grandparent"}, + {"event", "test.collection.grandparent"}, + {"event", "test.collection.parent"}, + {"event", "test.collection"}, + }, + { + {"subscribe", "test.collection.parent"}, + {"access", "test.collection.parent"}, + {"get", "test.collection.parent"}, + {"get", "test.collection"}, + {"response", "test.collection.parent"}, + {"subscribe", "test.collection.secondparent"}, + {"access", "test.collection.secondparent"}, + {"get", "test.collection.secondparent"}, + {"response", "test.collection.secondparent"}, + }, + { + {"subscribe", "test.collection.brokenchild"}, + {"access", "test.collection.brokenchild"}, + {"get", "test.collection.brokenchild"}, + {"get", "test.err.notFound"}, + {"response", "test.collection.brokenchild"}, + {"event", "test.collection.brokenchild"}, + {"noevent", "test.err.notFound"}, + }, + { + {"subscribe", "test.collection.soft"}, + {"access", "test.collection.soft"}, + {"get", "test.collection.soft"}, + {"response", "test.collection.soft"}, + {"event", "test.collection.soft"}, + {"nosubscription", "test.collection"}, + }, + // Cyclic collection tests + { + {"subscribe", "test.c.a"}, + {"access", "test.c.a"}, + {"get", "test.c.a"}, + {"response", "test.c.a"}, + }, + { + {"subscribe", "test.c.b"}, + {"access", "test.c.b"}, + {"get", "test.c.b"}, + {"get", "test.c.c"}, + {"response", "test.c.b"}, + }, + { + {"subscribe", "test.c.d"}, + {"access", "test.c.d"}, + {"get", "test.c.d"}, + {"get", "test.c.e"}, + {"get", "test.c.f"}, + {"response", "test.c.d"}, + }, + { + {"subscribe", "test.c.g"}, + {"access", "test.c.g"}, + {"get", "test.c.g"}, + {"get", "test.c.e"}, + {"get", "test.c.f"}, + {"get", "test.c.d"}, + {"response", "test.c.g"}, + }, + { + {"subscribe", "test.c.d"}, + {"access", "test.c.d"}, + {"get", "test.c.d"}, + {"subscribe", "test.c.h"}, + {"access", "test.c.h"}, + {"get", "test.c.e"}, + {"get", "test.c.h"}, + {"get", "test.c.f"}, + {"response", "test.c.d"}, + {"response", "test.c.h"}, + }, + // Access test + { + {"subscribe", "test.model.parent"}, + {"access", "test.model.parent"}, + {"get", "test.model.parent"}, + {"get", "test.model"}, + {"response", "test.model.parent"}, + {"subscribe", "test.model"}, + {"access", "test.model"}, + {"response", "test.model"}, + {"event", "test.model.parent"}, + {"event", "test.model"}, + }, + { + {"subscribe", "test.model.parent"}, + {"access", "test.model.parent"}, + {"get", "test.model.parent"}, + {"get", "test.model"}, + {"response", "test.model.parent"}, + {"subscribe", "test.model"}, + {"accessDenied", "test.model"}, + {"errorResponse", "test.model"}, + {"event", "test.model.parent"}, + {"event", "test.model"}, + }, + }, }, { - {"subscribe", "test.model.parent"}, - {"access", "test.model.parent"}, - {"get", "test.model.parent"}, - {"get", "test.model"}, - {"response", "test.model.parent"}, - {"subscribe", "test.model"}, - {"accessDenied", "test.model"}, - {"errorResponse", "test.model"}, - {"event", "test.model.parent"}, - {"event", "test.model"}, + "1.2.0", + [][]sequenceEvent{ + // Model tests + { + {"subscribe", "test.model.soft"}, + {"access", "test.model.soft"}, + {"get", "test.model.soft"}, + {"response", "test.model.soft"}, + {"event", "test.model.soft"}, + {"nosubscription", "test.model"}, + }, + { + {"subscribe", "test.model.soft.parent"}, + {"access", "test.model.soft.parent"}, + {"get", "test.model.soft.parent"}, + {"get", "test.model.soft"}, + {"response", "test.model.soft.parent"}, + {"event", "test.model.soft.parent"}, + {"event", "test.model.soft"}, + }, + }, }, } diff --git a/test/test.go b/test/test.go index ee3a760..e20a600 100644 --- a/test/test.go +++ b/test/test.go @@ -16,7 +16,8 @@ import ( const timeoutSeconds = 1 var ( - versionRequest = json.RawMessage(`{"protocol":"1.999.999"}`) + versionLatest = "1.999.999" + versionRequest = json.RawMessage(fmt.Sprintf(`{"protocol":"%s"}`, versionLatest)) versionResult = json.RawMessage(fmt.Sprintf(`{"protocol":"%s"}`, server.ProtocolVersion)) ) @@ -84,6 +85,20 @@ func (s *Session) Connect() *Conn { return c } +// ConnectWithVersion makes a new mock client websocket connection +// that handshakes with the version string provided. +func (s *Session) ConnectWithVersion(version string) *Conn { + c := s.connect(make(chan *ClientEvent, 256), nil) + + // Send version connect + creq := c.Request("version", struct { + Protocol string `json:"protocol"` + }{version}) + cresp := creq.GetResponse(s.t) + cresp.AssertResult(s.t, versionResult) + return c +} + // ConnectWithoutVersion makes a new mock client websocket connection // without any version handshake. func (s *Session) ConnectWithoutVersion() *Conn {