package pub import ( "context" "crypto" "github.com/go-fed/activity/vocab" "github.com/go-fed/httpsig" "net/http" "net/http/httptest" "net/url" "testing" ) func TestServeActivityPubObject(t *testing.T) { tests := []struct { name string app *MockApplication clock *MockClock input *http.Request expectedCode int expectedObjFn func() vocab.Serializer expectHandled bool }{ { name: "unsigned request", app: &MockApplication{ t: t, get: func(c context.Context, id *url.URL, rw RWType) (PubObject, error) { if rw != Read { t.Fatalf("expected RWType of %d, got %d", Read, rw) } else if s := id.String(); s != noteURIString { t.Fatalf("expected %s, got %s", noteURIString, s) } testNote = &vocab.Note{} testNote.SetId(noteIRI) testNote.AppendNameString(noteName) testNote.AppendContentString("This is a simple note") return testNote, nil }, owns: func(c context.Context, id *url.URL) bool { if s := id.String(); s != noteURIString { t.Fatalf("expected %s, got %s", noteURIString, s) } return true }, }, clock: &MockClock{now}, input: ActivityPubRequest(httptest.NewRequest("GET", noteURIString, nil)), expectedCode: http.StatusOK, expectedObjFn: func() vocab.Serializer { testNote = &vocab.Note{} testNote.SetId(noteIRI) testNote.AppendNameString(noteName) testNote.AppendContentString("This is a simple note") return testNote }, expectHandled: true, }, { name: "http signature request", input: Sign(ActivityPubRequest(httptest.NewRequest("GET", noteURIString, nil))), app: &MockApplication{ t: t, getPublicKey: func(c context.Context, publicKeyId string) (crypto.PublicKey, httpsig.Algorithm, *url.URL, error) { if publicKeyId != testPublicKeyId { t.Fatalf("expected %s, got %s", testPublicKeyId, publicKeyId) } return testPrivateKey.Public(), httpsig.RSA_SHA256, samIRI, nil }, getAsVerifiedUser: func(c context.Context, id, user *url.URL, rw RWType) (PubObject, error) { if rw != Read { t.Fatalf("expected RWType of %d, got %d", Read, rw) } else if s := id.String(); s != noteURIString { t.Fatalf("expected %s, got %s", noteURIString, s) } else if u := user.String(); u != samIRIString { t.Fatalf("expected %s, got %s", samIRIString, u) } testNote = &vocab.Note{} testNote.SetId(noteIRI) testNote.AppendNameString(noteName) testNote.AppendContentString("This is a simple note") return testNote, nil }, owns: func(c context.Context, id *url.URL) bool { if s := id.String(); s != noteURIString { t.Fatalf("expected %s, got %s", noteURIString, s) } return true }, }, clock: &MockClock{now}, expectedCode: http.StatusOK, expectedObjFn: func() vocab.Serializer { testNote = &vocab.Note{} testNote.SetId(noteIRI) testNote.AppendNameString(noteName) testNote.AppendContentString("This is a simple note") return testNote }, expectHandled: true, }, { name: "not owned", input: ActivityPubRequest(httptest.NewRequest("GET", noteURIString, nil)), app: &MockApplication{ t: t, owns: func(c context.Context, id *url.URL) bool { if s := id.String(); s != noteURIString { t.Fatalf("expected %s, got %s", noteURIString, s) } return false }, }, expectedCode: http.StatusNotFound, expectHandled: true, }, { name: "not activitypub get", input: httptest.NewRequest("GET", noteURIString, nil), expectHandled: false, }, { name: "bad http signature", input: BadSignature(ActivityPubRequest(httptest.NewRequest("GET", noteURIString, nil))), app: &MockApplication{ t: t, getPublicKey: func(c context.Context, publicKeyId string) (crypto.PublicKey, httpsig.Algorithm, *url.URL, error) { if publicKeyId != testPublicKeyId { t.Fatalf("expected %s, got %s", testPublicKeyId, publicKeyId) } return testPrivateKey.Public(), httpsig.RSA_SHA256, samIRI, nil }, owns: func(c context.Context, id *url.URL) bool { if s := id.String(); s != noteURIString { t.Fatalf("expected %s, got %s", noteURIString, s) } return true }, }, expectedCode: http.StatusForbidden, expectHandled: true, }, { name: "remove bto & bcc", app: &MockApplication{ t: t, get: func(c context.Context, id *url.URL, rw RWType) (PubObject, error) { if rw != Read { t.Fatalf("expected RWType of %d, got %d", Read, rw) } else if s := id.String(); s != noteURIString { t.Fatalf("expected %s, got %s", noteURIString, s) } testNote = &vocab.Note{} testNote.SetId(noteIRI) testNote.AppendNameString(noteName) testNote.AppendContentString("This is a simple note") testNote.AppendBtoIRI(samIRI) testNote.AppendBccIRI(sallyIRI) return testNote, nil }, owns: func(c context.Context, id *url.URL) bool { if s := id.String(); s != noteURIString { t.Fatalf("expected %s, got %s", noteURIString, s) } return true }, }, clock: &MockClock{now}, input: ActivityPubRequest(httptest.NewRequest("GET", noteURIString, nil)), expectedCode: http.StatusOK, expectedObjFn: func() vocab.Serializer { testNote = &vocab.Note{} testNote.SetId(noteIRI) testNote.AppendNameString(noteName) testNote.AppendContentString("This is a simple note") return testNote }, expectHandled: true, }, { name: "tombstone is status gone", app: &MockApplication{ t: t, get: func(c context.Context, id *url.URL, rw RWType) (PubObject, error) { if rw != Read { t.Fatalf("expected RWType of %d, got %d", Read, rw) } else if s := id.String(); s != testNewIRIString { t.Fatalf("expected %s, got %s", testNewIRIString, s) } tombstone := &vocab.Tombstone{} tombstone.SetId(testNewIRI) return tombstone, nil }, owns: func(c context.Context, id *url.URL) bool { if s := id.String(); s != testNewIRIString { t.Fatalf("expected %s, got %s", testNewIRIString, s) } return true }, }, clock: &MockClock{now}, input: ActivityPubRequest(httptest.NewRequest("GET", testNewIRIString, nil)), expectedCode: http.StatusGone, expectedObjFn: func() vocab.Serializer { tombstone := &vocab.Tombstone{} tombstone.SetId(testNewIRI) return tombstone }, expectHandled: true, }, } for _, test := range tests { t.Logf("Running table test case %q", test.name) resp := httptest.NewRecorder() fnUnderTest := ServeActivityPubObject(test.app, test.clock) handled, err := fnUnderTest(context.Background(), resp, test.input) if err != nil { t.Fatalf("(%q) %s", test.name, err) } else if handled != test.expectHandled { t.Fatalf("(%q) expected %v, got %v", test.name, test.expectHandled, handled) } else if test.expectedCode != 0 { if resp.Code != test.expectedCode { t.Fatalf("(%q) expected %d, got %d", test.name, test.expectedCode, resp.Code) } } else if test.expectedObjFn != nil { if err := VocabEquals(resp.Body, test.expectedObjFn()); err != nil { t.Fatalf("(%q) unexpected object: %s", test.name, err) } } } } func TestServeActivityPubObjectWithVerificationMethod(t *testing.T) { tests := []struct { name string app *MockApplication clock *MockClock verifier *MockSocialAPIVerifier input *http.Request expectedCode int expectedObjFn func() vocab.Serializer expectHandled bool }{ { name: "unsigned request", app: &MockApplication{ t: t, get: func(c context.Context, id *url.URL, rw RWType) (PubObject, error) { if rw != Read { t.Fatalf("expected RWType of %d, got %d", Read, rw) } else if s := id.String(); s != noteURIString { t.Fatalf("expected %s, got %s", noteURIString, s) } testNote = &vocab.Note{} testNote.SetId(noteIRI) testNote.AppendNameString(noteName) testNote.AppendContentString("This is a simple note") return testNote, nil }, owns: func(c context.Context, id *url.URL) bool { if s := id.String(); s != noteURIString { t.Fatalf("expected %s, got %s", noteURIString, s) } return true }, }, clock: &MockClock{now}, input: ActivityPubRequest(httptest.NewRequest("GET", noteURIString, nil)), expectedCode: http.StatusOK, expectedObjFn: func() vocab.Serializer { testNote = &vocab.Note{} testNote.SetId(noteIRI) testNote.AppendNameString(noteName) testNote.AppendContentString("This is a simple note") return testNote }, expectHandled: true, }, { name: "http signature request", input: Sign(ActivityPubRequest(httptest.NewRequest("GET", noteURIString, nil))), app: &MockApplication{ t: t, getPublicKey: func(c context.Context, publicKeyId string) (crypto.PublicKey, httpsig.Algorithm, *url.URL, error) { if publicKeyId != testPublicKeyId { t.Fatalf("expected %s, got %s", testPublicKeyId, publicKeyId) } return testPrivateKey.Public(), httpsig.RSA_SHA256, samIRI, nil }, getAsVerifiedUser: func(c context.Context, id, user *url.URL, rw RWType) (PubObject, error) { if rw != Read { t.Fatalf("expected RWType of %d, got %d", Read, rw) } else if s := id.String(); s != noteURIString { t.Fatalf("expected %s, got %s", noteURIString, s) } else if u := user.String(); u != samIRIString { t.Fatalf("expected %s, got %s", samIRIString, u) } testNote = &vocab.Note{} testNote.SetId(noteIRI) testNote.AppendNameString(noteName) testNote.AppendContentString("This is a simple note") return testNote, nil }, owns: func(c context.Context, id *url.URL) bool { if s := id.String(); s != noteURIString { t.Fatalf("expected %s, got %s", noteURIString, s) } return true }, }, clock: &MockClock{now}, expectedCode: http.StatusOK, expectedObjFn: func() vocab.Serializer { testNote = &vocab.Note{} testNote.SetId(noteIRI) testNote.AppendNameString(noteName) testNote.AppendContentString("This is a simple note") return testNote }, expectHandled: true, }, { name: "not owned", input: ActivityPubRequest(httptest.NewRequest("GET", noteURIString, nil)), app: &MockApplication{ t: t, owns: func(c context.Context, id *url.URL) bool { if s := id.String(); s != noteURIString { t.Fatalf("expected %s, got %s", noteURIString, s) } return false }, }, expectedCode: http.StatusNotFound, expectHandled: true, }, { name: "not activitypub get", input: httptest.NewRequest("GET", noteURIString, nil), expectHandled: false, }, { name: "bad http signature", input: BadSignature(ActivityPubRequest(httptest.NewRequest("GET", noteURIString, nil))), app: &MockApplication{ t: t, getPublicKey: func(c context.Context, publicKeyId string) (crypto.PublicKey, httpsig.Algorithm, *url.URL, error) { if publicKeyId != testPublicKeyId { t.Fatalf("expected %s, got %s", testPublicKeyId, publicKeyId) } return testPrivateKey.Public(), httpsig.RSA_SHA256, samIRI, nil }, owns: func(c context.Context, id *url.URL) bool { if s := id.String(); s != noteURIString { t.Fatalf("expected %s, got %s", noteURIString, s) } return true }, }, expectedCode: http.StatusForbidden, expectHandled: true, }, { name: "unsigned request passes verifier", app: &MockApplication{ t: t, getAsVerifiedUser: func(c context.Context, id, user *url.URL, rw RWType) (PubObject, error) { if rw != Read { t.Fatalf("expected RWType of %d, got %d", Read, rw) } else if s := id.String(); s != noteURIString { t.Fatalf("expected %s, got %s", noteURIString, s) } else if u := user.String(); u != samIRIString { t.Fatalf("expected %s, got %s", samIRIString, u) } testNote = &vocab.Note{} testNote.SetId(noteIRI) testNote.AppendNameString(noteName) testNote.AppendContentString("This is a simple note") return testNote, nil }, owns: func(c context.Context, id *url.URL) bool { if s := id.String(); s != noteURIString { t.Fatalf("expected %s, got %s", noteURIString, s) } return true }, }, clock: &MockClock{now}, verifier: &MockSocialAPIVerifier{ t: t, verify: func(r *http.Request) (*url.URL, bool, bool, error) { return samIRI, true, true, nil }, }, input: ActivityPubRequest(httptest.NewRequest("GET", noteURIString, nil)), expectedCode: http.StatusOK, expectedObjFn: func() vocab.Serializer { testNote = &vocab.Note{} testNote.SetId(noteIRI) testNote.AppendNameString(noteName) testNote.AppendContentString("This is a simple note") return testNote }, expectHandled: true, }, { name: "http signature request passes verifier", input: Sign(ActivityPubRequest(httptest.NewRequest("GET", noteURIString, nil))), app: &MockApplication{ t: t, getAsVerifiedUser: func(c context.Context, id, user *url.URL, rw RWType) (PubObject, error) { if rw != Read { t.Fatalf("expected RWType of %d, got %d", Read, rw) } else if s := id.String(); s != noteURIString { t.Fatalf("expected %s, got %s", noteURIString, s) } else if u := user.String(); u != samIRIString { t.Fatalf("expected %s, got %s", samIRIString, u) } testNote = &vocab.Note{} testNote.SetId(noteIRI) testNote.AppendNameString(noteName) testNote.AppendContentString("This is a simple note") return testNote, nil }, owns: func(c context.Context, id *url.URL) bool { if s := id.String(); s != noteURIString { t.Fatalf("expected %s, got %s", noteURIString, s) } return true }, }, clock: &MockClock{now}, verifier: &MockSocialAPIVerifier{ t: t, verify: func(r *http.Request) (*url.URL, bool, bool, error) { return samIRI, true, true, nil }, }, expectedCode: http.StatusOK, expectedObjFn: func() vocab.Serializer { testNote = &vocab.Note{} testNote.SetId(noteIRI) testNote.AppendNameString(noteName) testNote.AppendContentString("This is a simple note") return testNote }, expectHandled: true, }, { name: "verifier authed unauthz", app: &MockApplication{ t: t, owns: func(c context.Context, id *url.URL) bool { if s := id.String(); s != noteURIString { t.Fatalf("expected %s, got %s", noteURIString, s) } return true }, }, clock: &MockClock{now}, verifier: &MockSocialAPIVerifier{ t: t, verify: func(r *http.Request) (*url.URL, bool, bool, error) { return samIRI, true, false, nil }, }, input: ActivityPubRequest(httptest.NewRequest("GET", noteURIString, nil)), expectedCode: http.StatusForbidden, expectHandled: true, }, { name: "verifier unauthed unauthz", app: &MockApplication{ t: t, owns: func(c context.Context, id *url.URL) bool { if s := id.String(); s != noteURIString { t.Fatalf("expected %s, got %s", noteURIString, s) } return true }, }, clock: &MockClock{now}, verifier: &MockSocialAPIVerifier{ t: t, verify: func(r *http.Request) (*url.URL, bool, bool, error) { return nil, false, false, nil }, }, input: ActivityPubRequest(httptest.NewRequest("GET", noteURIString, nil)), expectedCode: http.StatusBadRequest, expectHandled: true, }, { name: "verifier unauthed authz unsigned fails", app: &MockApplication{ t: t, owns: func(c context.Context, id *url.URL) bool { if s := id.String(); s != noteURIString { t.Fatalf("expected %s, got %s", noteURIString, s) } return true }, }, clock: &MockClock{now}, verifier: &MockSocialAPIVerifier{ t: t, verify: func(r *http.Request) (*url.URL, bool, bool, error) { return nil, false, true, nil }, }, input: ActivityPubRequest(httptest.NewRequest("GET", noteURIString, nil)), expectedCode: http.StatusBadRequest, expectHandled: true, }, { name: "verifier unauthed authz signed success", input: Sign(ActivityPubRequest(httptest.NewRequest("GET", noteURIString, nil))), app: &MockApplication{ t: t, getPublicKey: func(c context.Context, publicKeyId string) (crypto.PublicKey, httpsig.Algorithm, *url.URL, error) { if publicKeyId != testPublicKeyId { t.Fatalf("expected %s, got %s", testPublicKeyId, publicKeyId) } return testPrivateKey.Public(), httpsig.RSA_SHA256, samIRI, nil }, getAsVerifiedUser: func(c context.Context, id, user *url.URL, rw RWType) (PubObject, error) { if rw != Read { t.Fatalf("expected RWType of %d, got %d", Read, rw) } else if s := id.String(); s != noteURIString { t.Fatalf("expected %s, got %s", noteURIString, s) } else if u := user.String(); u != samIRIString { t.Fatalf("expected %s, got %s", samIRIString, u) } testNote = &vocab.Note{} testNote.SetId(noteIRI) testNote.AppendNameString(noteName) testNote.AppendContentString("This is a simple note") return testNote, nil }, owns: func(c context.Context, id *url.URL) bool { if s := id.String(); s != noteURIString { t.Fatalf("expected %s, got %s", noteURIString, s) } return true }, }, clock: &MockClock{now}, verifier: &MockSocialAPIVerifier{ t: t, verify: func(r *http.Request) (*url.URL, bool, bool, error) { return nil, false, true, nil }, }, expectedCode: http.StatusOK, expectedObjFn: func() vocab.Serializer { testNote = &vocab.Note{} testNote.SetId(noteIRI) testNote.AppendNameString(noteName) testNote.AppendContentString("This is a simple note") return testNote }, expectHandled: true, }, { name: "verifier unauthed authz unsigned fails with bad impl returning user", app: &MockApplication{ t: t, owns: func(c context.Context, id *url.URL) bool { if s := id.String(); s != noteURIString { t.Fatalf("expected %s, got %s", noteURIString, s) } return true }, }, clock: &MockClock{now}, verifier: &MockSocialAPIVerifier{ t: t, verify: func(r *http.Request) (*url.URL, bool, bool, error) { return samIRI, false, true, nil }, }, input: ActivityPubRequest(httptest.NewRequest("GET", noteURIString, nil)), expectedCode: http.StatusBadRequest, expectHandled: true, }, { name: "remove bto & bcc", app: &MockApplication{ t: t, getPublicKey: func(c context.Context, publicKeyId string) (crypto.PublicKey, httpsig.Algorithm, *url.URL, error) { if publicKeyId != testPublicKeyId { t.Fatalf("expected %s, got %s", testPublicKeyId, publicKeyId) } return testPrivateKey.Public(), httpsig.RSA_SHA256, samIRI, nil }, getAsVerifiedUser: func(c context.Context, id, user *url.URL, rw RWType) (PubObject, error) { if rw != Read { t.Fatalf("expected RWType of %d, got %d", Read, rw) } else if s := id.String(); s != noteURIString { t.Fatalf("expected %s, got %s", noteURIString, s) } else if u := user.String(); u != samIRIString { t.Fatalf("expected %s, got %s", samIRIString, u) } testNote = &vocab.Note{} testNote.SetId(noteIRI) testNote.AppendNameString(noteName) testNote.AppendContentString("This is a simple note") testNote.AppendBtoIRI(samIRI) testNote.AppendBccIRI(sallyIRI) return testNote, nil }, owns: func(c context.Context, id *url.URL) bool { if s := id.String(); s != noteURIString { t.Fatalf("expected %s, got %s", noteURIString, s) } return true }, }, clock: &MockClock{now}, input: Sign(ActivityPubRequest(httptest.NewRequest("GET", noteURIString, nil))), expectedCode: http.StatusOK, expectedObjFn: func() vocab.Serializer { testNote = &vocab.Note{} testNote.SetId(noteIRI) testNote.AppendNameString(noteName) testNote.AppendContentString("This is a simple note") return testNote }, expectHandled: true, }, { name: "tombstone is status gone", app: &MockApplication{ t: t, getPublicKey: func(c context.Context, publicKeyId string) (crypto.PublicKey, httpsig.Algorithm, *url.URL, error) { if publicKeyId != testPublicKeyId { t.Fatalf("expected %s, got %s", testPublicKeyId, publicKeyId) } return testPrivateKey.Public(), httpsig.RSA_SHA256, samIRI, nil }, getAsVerifiedUser: func(c context.Context, id, user *url.URL, rw RWType) (PubObject, error) { if rw != Read { t.Fatalf("expected RWType of %d, got %d", Read, rw) } else if s := id.String(); s != testNewIRIString { t.Fatalf("expected %s, got %s", testNewIRIString, s) } else if u := user.String(); u != samIRIString { t.Fatalf("expected %s, got %s", samIRIString, u) } tombstone := &vocab.Tombstone{} tombstone.SetId(testNewIRI) return tombstone, nil }, owns: func(c context.Context, id *url.URL) bool { if s := id.String(); s != testNewIRIString { t.Fatalf("expected %s, got %s", testNewIRIString, s) } return true }, }, clock: &MockClock{now}, input: Sign(ActivityPubRequest(httptest.NewRequest("GET", testNewIRIString, nil))), expectedCode: http.StatusGone, expectedObjFn: func() vocab.Serializer { tombstone := &vocab.Tombstone{} tombstone.SetId(testNewIRI) return tombstone }, expectHandled: true, }, } for _, test := range tests { t.Logf("Running table test case %q", test.name) resp := httptest.NewRecorder() var fnUnderTest HandlerFunc if test.verifier != nil { verifierFn := func(c context.Context) SocialAPIVerifier { return test.verifier } fnUnderTest = ServeActivityPubObjectWithVerificationMethod(test.app, test.clock, verifierFn) } else { fnUnderTest = ServeActivityPubObjectWithVerificationMethod(test.app, test.clock, nil) } handled, err := fnUnderTest(context.Background(), resp, test.input) if err != nil { t.Fatalf("(%q) %s", test.name, err) } else if handled != test.expectHandled { t.Fatalf("(%q) expected %v, got %v", test.name, test.expectHandled, handled) } else if test.expectedCode != 0 { if resp.Code != test.expectedCode { t.Fatalf("(%q) expected %d, got %d", test.name, test.expectedCode, resp.Code) } } else if test.expectedObjFn != nil { if err := VocabEquals(resp.Body, test.expectedObjFn()); err != nil { t.Fatalf("(%q) unexpected object: %s", test.name, err) } } } }