package activitypub import ( "errors" "fmt" "reflect" "testing" "time" "unsafe" ) type visit struct { a1 unsafe.Pointer a2 unsafe.Pointer typ reflect.Type } type canErrorFunc func(format string, args ...interface{}) // See reflect.DeepEqual func assertDeepEquals(t canErrorFunc, x, y interface{}) bool { if x == nil || y == nil { return x == y } v1 := reflect.ValueOf(x) v2 := reflect.ValueOf(y) if v1.Type() != v2.Type() { t("%T != %T", x, y) return false } return deepValueEqual(t, v1, v2, make(map[visit]bool), 0) } // See reflect.deepValueEqual func deepValueEqual(t canErrorFunc, v1, v2 reflect.Value, visited map[visit]bool, depth int) bool { if !v1.IsValid() || !v2.IsValid() { return v1.IsValid() == v2.IsValid() } if v1.Type() != v2.Type() { t("types differ %s != %s", v1.Type().Name(), v2.Type().Name()) return false } hard := func(v1, v2 reflect.Value) bool { switch v1.Kind() { case reflect.Ptr: return false case reflect.Map, reflect.Slice, reflect.Interface: // Nil pointers cannot be cyclic. Avoid putting them in the visited map. return !v1.IsNil() && !v2.IsNil() } return false } if hard(v1, v2) { var addr1, addr2 unsafe.Pointer if v1.CanAddr() { addr1 = unsafe.Pointer(v1.UnsafeAddr()) } else { addr1 = unsafe.Pointer(v1.Pointer()) } if v2.CanAddr() { addr2 = unsafe.Pointer(v2.UnsafeAddr()) } else { addr2 = unsafe.Pointer(v2.Pointer()) } if uintptr(addr1) > uintptr(addr2) { // Canonicalize order to reduce number of entries in visited. // Assumes non-moving garbage collector. addr1, addr2 = addr2, addr1 } // Short circuit if references are already seen. typ := v1.Type() v := visit{addr1, addr2, typ} if visited[v] { return true } // Remember for later. visited[v] = true } switch v1.Kind() { case reflect.Array: for i := 0; i < v1.Len(); i++ { if !deepValueEqual(t, v1.Index(i), v2.Index(i), visited, depth+1) { t("Arrays not equal at index %d %s %s", i, v1.Index(i), v2.Index(i)) return false } } return true case reflect.Slice: if v1.IsNil() != v2.IsNil() { t("One of the slices is not nil %s[%d] vs %s[%d]", v1.Type().Name(), v1.Len(), v2.Type().Name(), v2.Len()) return false } if v1.Len() != v2.Len() { t("Slices lengths are different %s[%d] vs %s[%d]", v1.Type().Name(), v1.Len(), v2.Type().Name(), v2.Len()) return false } if v1.Pointer() == v2.Pointer() { return true } for i := 0; i < v1.Len(); i++ { if !deepValueEqual(t, v1.Index(i), v2.Index(i), visited, depth+1) { t("Slices elements at pos %d are not equal %#v vs %#v", i, v1.Index(i), v2.Index(i)) return false } } return true case reflect.Interface: if v1.IsNil() || v2.IsNil() { if v1.IsNil() == v2.IsNil() { return true } var isNil1, isNil2 string if v1.IsNil() { isNil1 = "is" } else { isNil1 = "is not" } if v2.IsNil() { isNil2 = "is" } else { isNil2 = "is not" } t("Interface '%s' %s nil and '%s' %s nil", v1.Type().Name(), isNil1, v2.Type().Name(), isNil2) return false } return deepValueEqual(t, v1.Elem(), v2.Elem(), visited, depth+1) case reflect.Ptr: if v1.Pointer() == v2.Pointer() { return true } return deepValueEqual(t, v1.Elem(), v2.Elem(), visited, depth+1) case reflect.Struct: for i, n := 0, v1.NumField(); i < n; i++ { var ( f1 = v1.Field(i) f2 = v2.Field(i) n1 = v1.Type().Field(i).Name n2 = v2.Type().Field(i).Name t1 = f1.Type().Name() t2 = f2.Type().Name() ) if !deepValueEqual(t, v1.Field(i), v2.Field(i), visited, depth+1) { t("Struct fields at pos %d %s[%s] and %s[%s] are not deeply equal", i, n1, t1, n2, t2) if f1.CanInterface() && f2.CanInterface() { t(" Values: %#v - %#v", v1.Field(i).Interface(), v2.Field(i).Interface()) } return false } } return true case reflect.Map: if v1.IsNil() != v2.IsNil() { t("Maps are not nil", v1.Type().Name(), v2.Type().Name()) return false } if v1.Len() != v2.Len() { t("Maps don't have the same length %d vs %d", v1.Len(), v2.Len()) return false } if v1.Pointer() == v2.Pointer() { return true } for _, k := range v1.MapKeys() { val1 := v1.MapIndex(k) val2 := v2.MapIndex(k) if !val1.IsValid() || !val2.IsValid() || !deepValueEqual(t, v1.MapIndex(k), v2.MapIndex(k), visited, depth+1) { t("Maps values at index %s are not equal", k.String()) return false } } return true case reflect.Func: if v1.IsNil() && v2.IsNil() { return true } // Can't do better than this: return false case reflect.String: return v1.String() == v2.String() case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return v1.Int() == v2.Int() case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: return v1.Uint() == v2.Uint() case reflect.Float32, reflect.Float64: return v1.Float() == v2.Float() case reflect.Bool: return v1.Bool() == v2.Bool() case reflect.Complex64, reflect.Complex128: return v1.Complex() == v2.Complex() } return false } type testPairs map[ActivityVocabularyType]reflect.Type var objectPtrType = reflect.TypeOf(new(*Object)).Elem() var tombstoneType = reflect.TypeOf(new(*Tombstone)).Elem() var profileType = reflect.TypeOf(new(*Profile)).Elem() var placeType = reflect.TypeOf(new(*Place)).Elem() var relationshipType = reflect.TypeOf(new(*Relationship)).Elem() var linkPtrType = reflect.TypeOf(new(*Link)).Elem() var mentionPtrType = reflect.TypeOf(new(*Mention)).Elem() var activityPtrType = reflect.TypeOf(new(*Activity)).Elem() var intransitiveActivityPtrType = reflect.TypeOf(new(*IntransitiveActivity)).Elem() var collectionPtrType = reflect.TypeOf(new(*Collection)).Elem() var collectionPagePtrType = reflect.TypeOf(new(*CollectionPage)).Elem() var orderedCollectionPtrType = reflect.TypeOf(new(*OrderedCollection)).Elem() var orderedCollectionPagePtrType = reflect.TypeOf(new(*OrderedCollectionPage)).Elem() var actorPtrType = reflect.TypeOf(new(*Actor)).Elem() var applicationPtrType = reflect.TypeOf(new(*Application)).Elem() var servicePtrType = reflect.TypeOf(new(*Service)).Elem() var personPtrType = reflect.TypeOf(new(*Person)).Elem() var groupPtrType = reflect.TypeOf(new(*Group)).Elem() var organizationPtrType = reflect.TypeOf(new(*Organization)).Elem() var acceptPtrType = reflect.TypeOf(new(*Accept)).Elem() var addPtrType = reflect.TypeOf(new(*Add)).Elem() var announcePtrType = reflect.TypeOf(new(*Announce)).Elem() var arrivePtrType = reflect.TypeOf(new(*Arrive)).Elem() var blockPtrType = reflect.TypeOf(new(*Block)).Elem() var createPtrType = reflect.TypeOf(new(*Create)).Elem() var deletePtrType = reflect.TypeOf(new(*Delete)).Elem() var dislikePtrType = reflect.TypeOf(new(*Dislike)).Elem() var flagPtrType = reflect.TypeOf(new(*Flag)).Elem() var followPtrType = reflect.TypeOf(new(*Follow)).Elem() var ignorePtrType = reflect.TypeOf(new(*Ignore)).Elem() var invitePtrType = reflect.TypeOf(new(*Invite)).Elem() var joinPtrType = reflect.TypeOf(new(*Join)).Elem() var leavePtrType = reflect.TypeOf(new(*Leave)).Elem() var likePtrType = reflect.TypeOf(new(*Like)).Elem() var listenPtrType = reflect.TypeOf(new(*Listen)).Elem() var movePtrType = reflect.TypeOf(new(*Move)).Elem() var offerPtrType = reflect.TypeOf(new(*Offer)).Elem() var questionPtrType = reflect.TypeOf(new(*Question)).Elem() var rejectPtrType = reflect.TypeOf(new(*Reject)).Elem() var readPtrType = reflect.TypeOf(new(*Read)).Elem() var removePtrType = reflect.TypeOf(new(*Remove)).Elem() var tentativeRejectPtrType = reflect.TypeOf(new(*TentativeReject)).Elem() var tentativeAcceptPtrType = reflect.TypeOf(new(*TentativeAccept)).Elem() var travelPtrType = reflect.TypeOf(new(*Travel)).Elem() var undoPtrType = reflect.TypeOf(new(*Undo)).Elem() var updatePtrType = reflect.TypeOf(new(*Update)).Elem() var viewPtrType = reflect.TypeOf(new(*View)).Elem() var tests = testPairs{ ObjectType: objectPtrType, ArticleType: objectPtrType, AudioType: objectPtrType, DocumentType: objectPtrType, ImageType: objectPtrType, NoteType: objectPtrType, PageType: objectPtrType, PlaceType: placeType, ProfileType: profileType, RelationshipType: relationshipType, TombstoneType: tombstoneType, VideoType: objectPtrType, LinkType: linkPtrType, MentionType: mentionPtrType, CollectionType: collectionPtrType, CollectionPageType: collectionPagePtrType, OrderedCollectionType: orderedCollectionPtrType, OrderedCollectionPageType: orderedCollectionPagePtrType, ActorType: actorPtrType, ApplicationType: applicationPtrType, ServiceType: servicePtrType, PersonType: personPtrType, GroupType: groupPtrType, OrganizationType: organizationPtrType, ActivityType: activityPtrType, IntransitiveActivityType: intransitiveActivityPtrType, AcceptType: acceptPtrType, AddType: addPtrType, AnnounceType: announcePtrType, ArriveType: arrivePtrType, BlockType: blockPtrType, CreateType: createPtrType, DeleteType: deletePtrType, DislikeType: dislikePtrType, FlagType: flagPtrType, FollowType: followPtrType, IgnoreType: ignorePtrType, InviteType: invitePtrType, JoinType: joinPtrType, LeaveType: leavePtrType, LikeType: likePtrType, ListenType: listenPtrType, MoveType: movePtrType, OfferType: offerPtrType, QuestionType: questionPtrType, RejectType: rejectPtrType, ReadType: readPtrType, RemoveType: removePtrType, TentativeRejectType: tentativeRejectPtrType, TentativeAcceptType: tentativeAcceptPtrType, TravelType: travelPtrType, UndoType: undoPtrType, UpdateType: updatePtrType, ViewType: viewPtrType, } func TestJSONGetItemByType(t *testing.T) { for typ, test := range tests { t.Run(string(typ), func(t *testing.T) { v, err := GetItemByType(typ) if err != nil { t.Error(err) } if reflect.TypeOf(v) != test { t.Errorf("Invalid type returned %T, expected %s", v, test.String()) } }) } } func TestUnmarshalJSON(t *testing.T) { tests := []struct { name string data []byte want Item err error }{ { name: "empty", data: []byte{'{', '}'}, want: nil, err: nil, }, { name: "IRI", data: []byte(`"http://example.com"`), want: IRI("http://example.com"), err: nil, }, { name: "IRIs", data: []byte(fmt.Sprintf("[%q, %q]", "http://example.com", "http://example.net")), want: ItemCollection{ IRI("http://example.com"), IRI("http://example.net"), }, err: nil, }, { name: "object", data: []byte(`{"type":"Note"}`), want: &Object{Type: NoteType}, err: nil, }, { name: "activity", data: []byte(`{"type":"Like"}`), want: &Activity{Type: LikeType}, err: nil, }, { name: "collection-2-items", data: []byte(`{ "@context": "https://www.w3.org/ns/activitystreams", "id": "https://federated.git/inbox", "type": "OrderedCollection", "updated": "2021-08-08T16:09:05Z", "first": "https://federated.git/inbox?maxItems=100", "totalItems": 2, "orderedItems": [ { "id": "https://federated.git/activities/07440c39-64b2-4492-89cf-f5c2872cf4ff", "type": "Create", "attributedTo": "https://federated.git/actors/b1757243-080a-49dc-b832-42905d554b91", "to": [ "https://www.w3.org/ns/activitystreams#Public" ], "cc": [ "https://federated.git/actors/b1757243-080a-49dc-b832-42905d554b91/followers" ], "published": "2021-08-08T16:09:05Z", "actor": "https://federated.git/actors/b1757243-080a-49dc-b832-42905d554b91", "object": "https://federated.git/objects/3eb69f77-3b08-4bf1-8760-c7333e2900c4" }, { "id": "https://federated.git/activities/ab9a5511-cdb5-4585-8a48-775d1bf20121", "type": "Like", "attributedTo": "https://federated.git/actors/b1757243-080a-49dc-b832-42905d554b91", "to": [ "https://www.w3.org/ns/activitystreams#Public", "https://federated.git/actors/b1757243-080a-49dc-b832-42905d554b91" ], "published": "2021-08-08T16:09:05Z", "actor": "https://federated.git/actors/b1757243-080a-49dc-b832-42905d554b91", "object": "https://federated.git/objects/3eb69f77-3b08-4bf1-8760-c7333e2900c4" }]}`), want: &OrderedCollection{ ID: "https://federated.git/inbox", Type: OrderedCollectionType, Updated: time.Date(2021, 8, 8, 16, 9, 5, 0, time.UTC), First: IRI("https://federated.git/inbox?maxItems=100"), OrderedItems: ItemCollection{ &Activity{ ID: "https://federated.git/activities/07440c39-64b2-4492-89cf-f5c2872cf4ff", Type: CreateType, AttributedTo: IRI("https://federated.git/actors/b1757243-080a-49dc-b832-42905d554b91"), To: ItemCollection{PublicNS}, CC: ItemCollection{IRI("https://federated.git/actors/b1757243-080a-49dc-b832-42905d554b91/followers")}, Published: time.Date(2021, 8, 8, 16, 9, 5, 0, time.UTC), Actor: IRI("https://federated.git/actors/b1757243-080a-49dc-b832-42905d554b91"), Object: IRI("https://federated.git/objects/3eb69f77-3b08-4bf1-8760-c7333e2900c4"), }, &Activity{ ID: "https://federated.git/activities/ab9a5511-cdb5-4585-8a48-775d1bf20121", Type: LikeType, AttributedTo: IRI("https://federated.git/actors/b1757243-080a-49dc-b832-42905d554b91"), To: ItemCollection{PublicNS, IRI("https://federated.git/actors/b1757243-080a-49dc-b832-42905d554b91")}, Published: time.Date(2021, 8, 8, 16, 9, 5, 0, time.UTC), Actor: IRI("https://federated.git/actors/b1757243-080a-49dc-b832-42905d554b91"), Object: IRI("https://federated.git/objects/3eb69f77-3b08-4bf1-8760-c7333e2900c4"), }, }, TotalItems: 2, }, err: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := UnmarshalJSON(tt.data) if (err != nil && tt.err == nil) || (err == nil && tt.err != nil) { if !errors.Is(err, tt.err) { t.Errorf("UnmarshalJSON() error = %v, wantErr %v", err, tt.err) } return } if !assertDeepEquals(t.Errorf, got, tt.want) { t.Errorf("UnmarshalJSON() got = %#v, want %#v", got, tt.want) } }) } } func TestJSONGetDuration(t *testing.T) { t.Skipf("TODO") } func TestJSONGetInt(t *testing.T) { } func TestJSONGetIRI(t *testing.T) { t.Skipf("TODO") } func TestJSONGetItem(t *testing.T) { t.Skipf("TODO") } func TestJSONGetItems(t *testing.T) { } func TestJSONGetLangRefField(t *testing.T) { t.Skipf("TODO") } func TestJSONGetMimeType(t *testing.T) { t.Skipf("TODO") } func TestJSONGetID(t *testing.T) { t.Skipf("TODO") } func TestJSONGetNaturalLanguageField(t *testing.T) { t.Skipf("TODO") } func TestJSONGetString(t *testing.T) { t.Skipf("TODO") } func TestJSONGetTime(t *testing.T) { t.Skipf("TODO") } func TestJSONGetType(t *testing.T) { t.Skipf("TODO") } func TestJSONGetURIItem(t *testing.T) { t.Skipf("TODO") } func TestJSONUnmarshalToItem(t *testing.T) { t.Skipf("TODO") } func TestJSONGetActorEndpoints(t *testing.T) { t.Skipf("TODO") } func TestJSONGetBoolean(t *testing.T) { t.Skipf("TODO") } func TestJSONGetBytes(t *testing.T) { t.Skipf("TODO") } func TestJSONGetFloat(t *testing.T) { t.Skipf("TODO") } func TestJSONGetPublicKey(t *testing.T) { t.Skipf("TODO") } func TestJSONGetStreams(t *testing.T) { t.Skipf("TODO") }