Add a layer of safety around required ActivityPub Actor fields (#4703)
* fix(ap): add safe constructors, getters, and validators to ActivityPub actors to address #4701 * chore: replace nil check with the new Validate() method * chore(test): added test to verify the values extracted from actors * fix: return the specific error type + test for it * fix(ap): add recovery for each AP inbox worker for a worst case scenario * fix(ap): add additional safe accessor methods to other AP entities other than actors * chore(tests): add tests for the new safe accessors * fix(ap): handle empty public keys in AP actors
This commit is contained in:
@@ -0,0 +1,251 @@
|
||||
package inbox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/go-fed/activity/streams"
|
||||
)
|
||||
|
||||
func mustParseURL(s string) *url.URL {
|
||||
u, err := url.Parse(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
// These tests verify that handler functions don't panic when given
|
||||
// ActivityPub activities with nil or missing properties that could
|
||||
// cause nil pointer dereferences.
|
||||
|
||||
func TestHandleFollowWithNilObject(t *testing.T) {
|
||||
activity := streams.NewActivityStreamsFollow()
|
||||
// Don't set object or actor - they will be nil
|
||||
|
||||
// This should return an error, not panic
|
||||
err := handleFollowInboxRequest(context.Background(), activity)
|
||||
if err == nil {
|
||||
t.Error("handleFollowInboxRequest with nil object should return error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleFollowWithEmptyObject(t *testing.T) {
|
||||
activity := streams.NewActivityStreamsFollow()
|
||||
object := streams.NewActivityStreamsObjectProperty()
|
||||
activity.SetActivityStreamsObject(object)
|
||||
// Object is set but empty (no items)
|
||||
|
||||
err := handleFollowInboxRequest(context.Background(), activity)
|
||||
if err == nil {
|
||||
t.Error("handleFollowInboxRequest with empty object should return error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleFollowWithNilActorIRI(t *testing.T) {
|
||||
activity := streams.NewActivityStreamsFollow()
|
||||
|
||||
// Set a valid object with IRI
|
||||
object := streams.NewActivityStreamsObjectProperty()
|
||||
objectNote := streams.NewActivityStreamsNote()
|
||||
objectID := streams.NewJSONLDIdProperty()
|
||||
objectID.SetIRI(mustParseURL("https://example.com/note/1"))
|
||||
objectNote.SetJSONLDId(objectID)
|
||||
object.AppendActivityStreamsNote(objectNote)
|
||||
activity.SetActivityStreamsObject(object)
|
||||
|
||||
// Actor is nil
|
||||
err := handleFollowInboxRequest(context.Background(), activity)
|
||||
if err == nil {
|
||||
t.Error("handleFollowInboxRequest with nil actor should return error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleAnnounceWithNilObject(t *testing.T) {
|
||||
activity := streams.NewActivityStreamsAnnounce()
|
||||
// Don't set object or actor
|
||||
|
||||
err := handleAnnounceRequest(context.Background(), activity)
|
||||
if err == nil {
|
||||
t.Error("handleAnnounceRequest with nil object should return error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleAnnounceWithEmptyObject(t *testing.T) {
|
||||
activity := streams.NewActivityStreamsAnnounce()
|
||||
object := streams.NewActivityStreamsObjectProperty()
|
||||
activity.SetActivityStreamsObject(object)
|
||||
|
||||
err := handleAnnounceRequest(context.Background(), activity)
|
||||
if err == nil {
|
||||
t.Error("handleAnnounceRequest with empty object should return error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleAnnounceWithNilActorIRI(t *testing.T) {
|
||||
activity := streams.NewActivityStreamsAnnounce()
|
||||
|
||||
// Set object with IRI
|
||||
object := streams.NewActivityStreamsObjectProperty()
|
||||
object.AppendIRI(mustParseURL("https://example.com/note/1"))
|
||||
activity.SetActivityStreamsObject(object)
|
||||
|
||||
// Actor is nil
|
||||
err := handleAnnounceRequest(context.Background(), activity)
|
||||
if err == nil {
|
||||
t.Error("handleAnnounceRequest with nil actor should return error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleLikeWithNilObject(t *testing.T) {
|
||||
activity := streams.NewActivityStreamsLike()
|
||||
// Don't set object or actor
|
||||
|
||||
err := handleLikeRequest(context.Background(), activity)
|
||||
if err == nil {
|
||||
t.Error("handleLikeRequest with nil object should return error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleLikeWithEmptyObject(t *testing.T) {
|
||||
activity := streams.NewActivityStreamsLike()
|
||||
object := streams.NewActivityStreamsObjectProperty()
|
||||
activity.SetActivityStreamsObject(object)
|
||||
|
||||
err := handleLikeRequest(context.Background(), activity)
|
||||
if err == nil {
|
||||
t.Error("handleLikeRequest with empty object should return error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleLikeWithNilActorIRI(t *testing.T) {
|
||||
activity := streams.NewActivityStreamsLike()
|
||||
|
||||
// Set object with IRI
|
||||
object := streams.NewActivityStreamsObjectProperty()
|
||||
object.AppendIRI(mustParseURL("https://example.com/note/1"))
|
||||
activity.SetActivityStreamsObject(object)
|
||||
|
||||
// Actor is nil
|
||||
err := handleLikeRequest(context.Background(), activity)
|
||||
if err == nil {
|
||||
t.Error("handleLikeRequest with nil actor should return error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleCreateWithNilId(t *testing.T) {
|
||||
activity := streams.NewActivityStreamsCreate()
|
||||
// Don't set JSONLD ID
|
||||
|
||||
err := handleCreateRequest(context.Background(), activity)
|
||||
if err == nil {
|
||||
t.Error("handleCreateRequest with nil ID should return error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleCreateWithIdButNilIRI(t *testing.T) {
|
||||
activity := streams.NewActivityStreamsCreate()
|
||||
id := streams.NewJSONLDIdProperty()
|
||||
// Set the ID property but don't set an IRI on it
|
||||
activity.SetJSONLDId(id)
|
||||
|
||||
err := handleCreateRequest(context.Background(), activity)
|
||||
if err == nil {
|
||||
t.Error("handleCreateRequest with ID but nil IRI should return error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleUpdateWithNilObject(t *testing.T) {
|
||||
activity := streams.NewActivityStreamsUpdate()
|
||||
// Don't set object - should return nil (not an error, just skip)
|
||||
|
||||
// This should not panic and should return nil since we only care about Person updates
|
||||
err := handleUpdateRequest(context.Background(), activity)
|
||||
if err != nil {
|
||||
t.Errorf("handleUpdateRequest with nil object should return nil (skip), got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleUpdateWithEmptyObject(t *testing.T) {
|
||||
activity := streams.NewActivityStreamsUpdate()
|
||||
object := streams.NewActivityStreamsObjectProperty()
|
||||
activity.SetActivityStreamsObject(object)
|
||||
|
||||
// Should return nil since empty object means it's not a Person update
|
||||
err := handleUpdateRequest(context.Background(), activity)
|
||||
if err != nil {
|
||||
t.Errorf("handleUpdateRequest with empty object should return nil (skip), got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleUpdateWithNonPersonObject(t *testing.T) {
|
||||
activity := streams.NewActivityStreamsUpdate()
|
||||
object := streams.NewActivityStreamsObjectProperty()
|
||||
note := streams.NewActivityStreamsNote()
|
||||
object.AppendActivityStreamsNote(note)
|
||||
activity.SetActivityStreamsObject(object)
|
||||
|
||||
// Should return nil since it's not a Person update
|
||||
err := handleUpdateRequest(context.Background(), activity)
|
||||
if err != nil {
|
||||
t.Errorf("handleUpdateRequest with non-Person object should return nil (skip), got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestNilSafetyNoPanic verifies that none of the handlers panic when given
|
||||
// completely empty activities. This is the most important test - we want to
|
||||
// ensure that malformed ActivityPub payloads don't crash the server.
|
||||
func TestNilSafetyNoPanic(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("Follow with nil properties", func(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("handleFollowInboxRequest panicked: %v", r)
|
||||
}
|
||||
}()
|
||||
activity := streams.NewActivityStreamsFollow()
|
||||
_ = handleFollowInboxRequest(ctx, activity)
|
||||
})
|
||||
|
||||
t.Run("Announce with nil properties", func(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("handleAnnounceRequest panicked: %v", r)
|
||||
}
|
||||
}()
|
||||
activity := streams.NewActivityStreamsAnnounce()
|
||||
_ = handleAnnounceRequest(ctx, activity)
|
||||
})
|
||||
|
||||
t.Run("Like with nil properties", func(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("handleLikeRequest panicked: %v", r)
|
||||
}
|
||||
}()
|
||||
activity := streams.NewActivityStreamsLike()
|
||||
_ = handleLikeRequest(ctx, activity)
|
||||
})
|
||||
|
||||
t.Run("Create with nil properties", func(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("handleCreateRequest panicked: %v", r)
|
||||
}
|
||||
}()
|
||||
activity := streams.NewActivityStreamsCreate()
|
||||
_ = handleCreateRequest(ctx, activity)
|
||||
})
|
||||
|
||||
t.Run("Update with nil properties", func(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("handleUpdateRequest panicked: %v", r)
|
||||
}
|
||||
}()
|
||||
activity := streams.NewActivityStreamsUpdate()
|
||||
_ = handleUpdateRequest(ctx, activity)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user