package streams import ( "context" "encoding/json" "github.com/go-fed/activity/streams/vocab" "github.com/go-test/deep" "net/url" "sort" "testing" ) // IsKnownResolverError returns true if it is known that an example from // GetTestTable will trigger a JSONResolver error. func IsKnownResolverError(t TestTable) (isError bool, reason string) { isError = true switch t.name { case "Example 61": reason = "no 'type' property is on the root object" case "Example 62": reason = "an unknown 'type' property is on the root object" case "Example 153": reason = "no 'type' property is on the root object" default: isError = false } return } // SerializeForTest calls Serialize and stabilizes the @context value for // testing purposes. func SerializeForTest(a vocab.Type) (m map[string]interface{}, e error) { m, e = Serialize(a) if e != nil { return } ctx, ok := m["@context"] if !ok { return } arr, ok := ctx.([]interface{}) if !ok { return } var s []string var o []interface{} for _, v := range arr { if str, ok := v.(string); ok { s = append(s, str) } else { o = append(o, v) } } sort.Sort(sort.StringSlice(s)) newArr := make([]interface{}, 0, len(arr)) for _, v := range s { newArr = append(newArr, v) } for _, v := range o { newArr = append(newArr, v) } m["@context"] = newArr return } // PostSerializationAdjustment is needed in rare cases when a test example // requires post processing on the serialized map to match expectations. func PostSerializationAdjustment(t TestTable, m map[string]interface{}) (map[string]interface{}, string) { adjustReason := "" switch t.name { case "Service w/ Multiple schema:PropertyValue Attachments": m["@context"] = []interface{}{ "https://www.w3.org/ns/activitystreams", map[string]interface{}{ "schema": "https://schema.org#", "PropertyValue": "schema:PropertyValue", "value": "schema:value", }, } adjustReason = "go-fed has no way of knowing that schema.org types need to be in the @context" } return m, adjustReason } func makeResolver(t *testing.T, tc TestTable, expected []byte) (*JSONResolver, error) { resFn := func(s vocab.Type) error { return nil } if t != nil { resFn = func(s vocab.Type) error { m, err := SerializeForTest(s) if err != nil { return err } m, adjustReason := PostSerializationAdjustment(tc, m) if len(adjustReason) > 0 { t.Logf("%s: Post-serialization adjustment: %s", tc.name, adjustReason) } actual, err := json.Marshal(m) if err != nil { t.Errorf("json.Marshal returned error: %v", err) } if diff, err := GetJSONDiff(actual, expected); err == nil && diff != nil { t.Error("Serialize JSON equality is false") for _, d := range diff { t.Log(d) } } else if err != nil { t.Errorf("GetJSONDiff returned error: %v", err) } return nil } } return NewJSONResolver( func(c context.Context, x vocab.ActivityStreamsAccept) error { return resFn(x) }, func(c context.Context, x vocab.ActivityStreamsActivity) error { return resFn(x) }, func(c context.Context, x vocab.ActivityStreamsAdd) error { return resFn(x) }, func(c context.Context, x vocab.ActivityStreamsAnnounce) error { return resFn(x) }, func(c context.Context, x vocab.ActivityStreamsApplication) error { return resFn(x) }, func(c context.Context, x vocab.ActivityStreamsArrive) error { return resFn(x) }, func(c context.Context, x vocab.ActivityStreamsArticle) error { return resFn(x) }, func(c context.Context, x vocab.ActivityStreamsAudio) error { return resFn(x) }, func(c context.Context, x vocab.ActivityStreamsBlock) error { return resFn(x) }, func(c context.Context, x vocab.ActivityStreamsCollection) error { return resFn(x) }, func(c context.Context, x vocab.ActivityStreamsCollectionPage) error { return resFn(x) }, func(c context.Context, x vocab.ActivityStreamsCreate) error { return resFn(x) }, func(c context.Context, x vocab.ActivityStreamsDelete) error { return resFn(x) }, func(c context.Context, x vocab.ActivityStreamsDislike) error { return resFn(x) }, func(c context.Context, x vocab.ActivityStreamsDocument) error { return resFn(x) }, func(c context.Context, x vocab.ActivityStreamsEvent) error { return resFn(x) }, func(c context.Context, x vocab.ActivityStreamsFlag) error { return resFn(x) }, func(c context.Context, x vocab.ActivityStreamsFollow) error { return resFn(x) }, func(c context.Context, x vocab.ActivityStreamsGroup) error { return resFn(x) }, func(c context.Context, x vocab.ActivityStreamsIgnore) error { return resFn(x) }, func(c context.Context, x vocab.ActivityStreamsImage) error { return resFn(x) }, func(c context.Context, x vocab.ActivityStreamsIntransitiveActivity) error { return resFn(x) }, func(c context.Context, x vocab.ActivityStreamsInvite) error { return resFn(x) }, func(c context.Context, x vocab.ActivityStreamsJoin) error { return resFn(x) }, func(c context.Context, x vocab.ActivityStreamsLeave) error { return resFn(x) }, func(c context.Context, x vocab.ActivityStreamsLike) error { return resFn(x) }, func(c context.Context, x vocab.ActivityStreamsLink) error { return resFn(x) }, func(c context.Context, x vocab.ActivityStreamsListen) error { return resFn(x) }, func(c context.Context, x vocab.ActivityStreamsMention) error { return resFn(x) }, func(c context.Context, x vocab.ActivityStreamsMove) error { return resFn(x) }, func(c context.Context, x vocab.ActivityStreamsNote) error { return resFn(x) }, func(c context.Context, x vocab.ActivityStreamsObject) error { return resFn(x) }, func(c context.Context, x vocab.ActivityStreamsOffer) error { return resFn(x) }, func(c context.Context, x vocab.ActivityStreamsOrderedCollection) error { return resFn(x) }, func(c context.Context, x vocab.ActivityStreamsOrderedCollectionPage) error { return resFn(x) }, func(c context.Context, x vocab.ActivityStreamsOrganization) error { return resFn(x) }, func(c context.Context, x vocab.ActivityStreamsPage) error { return resFn(x) }, func(c context.Context, x vocab.ActivityStreamsPerson) error { return resFn(x) }, func(c context.Context, x vocab.ActivityStreamsPlace) error { return resFn(x) }, func(c context.Context, x vocab.ActivityStreamsProfile) error { return resFn(x) }, func(c context.Context, x vocab.ActivityStreamsQuestion) error { return resFn(x) }, func(c context.Context, x vocab.ActivityStreamsRead) error { return resFn(x) }, func(c context.Context, x vocab.ActivityStreamsReject) error { return resFn(x) }, func(c context.Context, x vocab.ActivityStreamsRelationship) error { return resFn(x) }, func(c context.Context, x vocab.ActivityStreamsRemove) error { return resFn(x) }, func(c context.Context, x vocab.ActivityStreamsService) error { return resFn(x) }, func(c context.Context, x vocab.ActivityStreamsTentativeAccept) error { return resFn(x) }, func(c context.Context, x vocab.ActivityStreamsTentativeReject) error { return resFn(x) }, func(c context.Context, x vocab.ActivityStreamsTombstone) error { return resFn(x) }, func(c context.Context, x vocab.ActivityStreamsTravel) error { return resFn(x) }, func(c context.Context, x vocab.ActivityStreamsUndo) error { return resFn(x) }, func(c context.Context, x vocab.ActivityStreamsUpdate) error { return resFn(x) }, func(c context.Context, x vocab.ActivityStreamsVideo) error { return resFn(x) }, func(c context.Context, x vocab.ActivityStreamsView) error { return resFn(x) }, ) } func TestJSONResolver(t *testing.T) { for _, example := range GetTestTable() { example := example // shadow loop variable t.Run(example.name, func(t *testing.T) { if skip, reason := IsKnownResolverError(example); skip { t.Skipf("it is known an error will be returned because %q", reason) return } ex := []byte(example.expectedJSON) r, err := makeResolver(t, example, ex) if err != nil { t.Errorf("Cannot create JSONResolver: %v", err) return } var m map[string]interface{} err = json.Unmarshal(ex, &m) if err != nil { t.Errorf("Cannot json.Unmarshal: %v", err) return } err = r.Resolve(context.Background(), m) if err != nil { t.Errorf("Cannot JSONResolver.Deserialize: %v", err) return } }) } } func TestJSONResolverErrors(t *testing.T) { for _, example := range GetTestTable() { example := example // shadow loop variable t.Run(example.name, func(t *testing.T) { isError, reason := IsKnownResolverError(example) if !isError { t.Skip("no expected error") return } ex := []byte(example.expectedJSON) r, err := makeResolver(nil, example, nil) if err != nil { t.Errorf("Cannot create JSONResolver: %v", err) return } var m map[string]interface{} err = json.Unmarshal([]byte(ex), &m) if err != nil { t.Errorf("Cannot json.Unmarshal: %v", err) return } t.Logf("Expecting error because %q", reason) err = r.Resolve(context.Background(), m) if err == nil { t.Error("Expected error, but nil was returned.") } else { t.Logf("Returned error was %v", err) } }) } } func TestNulls(t *testing.T) { makeIRI := func(path string) *url.URL { return &url.URL{ Scheme: "https", Host: "example.com", Path: path, } } var ( samIRIInbox = makeIRI("/sam/inbox") samIRI = makeIRI("/sam") noteIRI = makeIRI("/note/123") sallyIRI = makeIRI("/sally") activityIRI = makeIRI("/test/new/iri") ) noteIdProperty := NewJSONLDIdProperty() noteIdProperty.SetIRI(noteIRI) expectedNote := NewActivityStreamsNote() expectedNote.SetJSONLDId(noteIdProperty) noteNameProperty := NewActivityStreamsNameProperty() noteNameProperty.AppendXMLSchemaString("A Note") expectedNote.SetActivityStreamsName(noteNameProperty) noteContentProperty := NewActivityStreamsContentProperty() noteContentProperty.AppendXMLSchemaString("This is a simple note") expectedNote.SetActivityStreamsContent(noteContentProperty) noteToProperty := NewActivityStreamsToProperty() expectedSamActor := NewActivityStreamsPerson() samInboxProperty := NewActivityStreamsInboxProperty() samInboxProperty.SetIRI(samIRIInbox) expectedSamActor.SetActivityStreamsInbox(samInboxProperty) samIdProperty := NewJSONLDIdProperty() samIdProperty.SetIRI(samIRI) expectedSamActor.SetJSONLDId(samIdProperty) noteToProperty.AppendActivityStreamsPerson(expectedSamActor) expectedNote.SetActivityStreamsTo(noteToProperty) expectedUpdate := NewActivityStreamsUpdate() sallyIdProperty := NewJSONLDIdProperty() sallyIdProperty.SetIRI(sallyIRI) sallyPerson := NewActivityStreamsPerson() sallyPerson.SetJSONLDId(sallyIdProperty) sallyActor := NewActivityStreamsActorProperty() sallyActor.AppendActivityStreamsPerson(sallyPerson) expectedUpdate.SetActivityStreamsActor(sallyActor) summaryProperty := NewActivityStreamsSummaryProperty() summaryProperty.AppendXMLSchemaString("Sally updated her note") expectedUpdate.SetActivityStreamsSummary(summaryProperty) updateIdProperty := NewJSONLDIdProperty() updateIdProperty.SetIRI(activityIRI) expectedUpdate.SetJSONLDId(updateIdProperty) objectNote := NewActivityStreamsObjectProperty() objectNote.AppendActivityStreamsNote(expectedNote) expectedUpdate.SetActivityStreamsObject(objectNote) // Variable to aid in deserialization in tests tables := []struct { name string expected vocab.Type callback func(*vocab.Type) interface{} input string inputWithoutNulls string }{ { name: "JSON nulls are not preserved", expected: expectedUpdate, callback: func(actual *vocab.Type) interface{} { return func(c context.Context, v vocab.ActivityStreamsUpdate) error { *actual = v return nil } }, input: ` { "@context": "https://www.w3.org/ns/activitystreams", "summary": "Sally updated her note", "type": "Update", "actor": "https://example.com/sally", "id": "https://example.com/test/new/iri", "object": { "id": "https://example.com/note/123", "type": "Note", "to": { "id": "https://example.com/sam", "inbox": "https://example.com/sam/inbox", "type": "Person", "name": null } } } `, inputWithoutNulls: ` { "@context": "https://www.w3.org/ns/activitystreams", "summary": "Sally updated her note", "type": "Update", "actor": "https://example.com/sally", "id": "https://example.com/test/new/iri", "object": { "id": "https://example.com/note/123", "type": "Note", "to": { "id": "https://example.com/sam", "inbox": "https://example.com/sam/inbox", "type": "Person" } } } `, }, } for _, r := range tables { r := r // shadow loop variable t.Run(r.name, func(t *testing.T) { var actual vocab.Type res, err := NewJSONResolver(r.callback(&actual)) if err != nil { t.Errorf("cannot create resolver: %s", err) return } var m map[string]interface{} err = json.Unmarshal([]byte(r.input), &m) if err != nil { t.Errorf("Cannot json.Unmarshal: %v", err) return } err = res.Resolve(context.Background(), m) if err != nil { t.Errorf("Cannot Deserialize: %v", err) return } if diff := deep.Equal(actual, r.expected); diff != nil { t.Error("Deserialize deep equal is false") for _, d := range diff { t.Log(d) } } m, err = SerializeForTest(actual) if err != nil { t.Errorf("Cannot Serialize: %v", err) return } reser, err := json.Marshal(m) if err != nil { t.Errorf("Cannot json.Marshal: %v", err) return } if diff, err := GetJSONDiff(reser, []byte(r.inputWithoutNulls)); err == nil && diff != nil { t.Error("Serialize JSON equality is false") for _, d := range diff { t.Log(d) } } else if err != nil { t.Errorf("GetJSONDiff returned error: %v", err) } }) } } func GetJSONDiff(str1, str2 []byte) ([]string, error) { var i1 interface{} var i2 interface{} err := json.Unmarshal(str1, &i1) if err != nil { return nil, err } err = json.Unmarshal(str2, &i2) if err != nil { return nil, err } return deep.Equal(i1, i2), nil }