Add test cases for remote actor deletion

This commit is contained in:
Thomas Sileo 2022-08-18 08:32:30 +02:00
parent 228de1b83a
commit 8e57bb9245
3 changed files with 190 additions and 26 deletions

View file

@ -12,24 +12,15 @@ from app import activitypub as ap
from app import models from app import models
from app.actor import LOCAL_ACTOR from app.actor import LOCAL_ACTOR
from app.ap_object import RemoteObject from app.ap_object import RemoteObject
from app.database import AsyncSession
from app.incoming_activities import fetch_next_incoming_activity
from app.incoming_activities import process_next_incoming_activity
from tests import factories from tests import factories
from tests.utils import mock_httpsig_checker from tests.utils import mock_httpsig_checker
from tests.utils import run_async from tests.utils import run_process_next_incoming_activity
from tests.utils import setup_inbox_delete from tests.utils import setup_inbox_delete
from tests.utils import setup_remote_actor from tests.utils import setup_remote_actor
from tests.utils import setup_remote_actor_as_follower from tests.utils import setup_remote_actor_as_follower
from tests.utils import setup_remote_actor_as_following from tests.utils import setup_remote_actor_as_following
async def _process_next_incoming_activity(db_session: AsyncSession) -> None:
next_activity = await fetch_next_incoming_activity(db_session)
assert next_activity
await process_next_incoming_activity(db_session, next_activity)
def test_inbox_requires_httpsig( def test_inbox_requires_httpsig(
client: TestClient, client: TestClient,
): ):
@ -70,10 +61,10 @@ def test_inbox_incoming_follow_request(
json=follow_activity.ap_object, json=follow_activity.ap_object,
) )
# Then the server returns a 204 # Then the server returns a 202
assert response.status_code == 202 assert response.status_code == 202
run_async(_process_next_incoming_activity) run_process_next_incoming_activity()
# And the actor was saved in DB # And the actor was saved in DB
saved_actor = db.execute(select(models.Actor)).scalar_one() saved_actor = db.execute(select(models.Actor)).scalar_one()
@ -127,11 +118,11 @@ def test_inbox_incoming_follow_request__manually_approves_followers(
json=follow_activity.ap_object, json=follow_activity.ap_object,
) )
# Then the server returns a 204 # Then the server returns a 202
assert response.status_code == 202 assert response.status_code == 202
with mock.patch("app.boxes.MANUALLY_APPROVES_FOLLOWERS", True): with mock.patch("app.boxes.MANUALLY_APPROVES_FOLLOWERS", True):
run_async(_process_next_incoming_activity) run_process_next_incoming_activity()
# And the actor was saved in DB # And the actor was saved in DB
saved_actor = db.execute(select(models.Actor)).scalar_one() saved_actor = db.execute(select(models.Actor)).scalar_one()
@ -183,10 +174,10 @@ def test_inbox_accept_follow_request(
json=accept_activity.ap_object, json=accept_activity.ap_object,
) )
# Then the server returns a 204 # Then the server returns a 202
assert response.status_code == 202 assert response.status_code == 202
run_async(_process_next_incoming_activity) run_process_next_incoming_activity()
# And the Accept activity was saved in the inbox # And the Accept activity was saved in the inbox
inbox_activity = db.execute(select(models.InboxObject)).scalar_one() inbox_activity = db.execute(select(models.InboxObject)).scalar_one()
@ -229,11 +220,11 @@ def test_inbox__create_from_follower(
json=ro.ap_object, json=ro.ap_object,
) )
# Then the server returns a 204 # Then the server returns a 202
assert response.status_code == 202 assert response.status_code == 202
# And when processing the incoming activity # And when processing the incoming activity
run_async(_process_next_incoming_activity) run_process_next_incoming_activity()
# Then the Create activity was saved # Then the Create activity was saved
create_activity_from_inbox: models.InboxObject | None = db.execute( create_activity_from_inbox: models.InboxObject | None = db.execute(
@ -283,11 +274,11 @@ def test_inbox__create_already_deleted_object(
json=ro.ap_object, json=ro.ap_object,
) )
# Then the server returns a 204 # Then the server returns a 202
assert response.status_code == 202 assert response.status_code == 202
# And when processing the incoming activity # And when processing the incoming activity
run_async(_process_next_incoming_activity) run_process_next_incoming_activity()
# Then the Create activity was saved # Then the Create activity was saved
create_activity_from_inbox: models.InboxObject | None = db.execute( create_activity_from_inbox: models.InboxObject | None = db.execute(
@ -339,11 +330,11 @@ def test_inbox__actor_is_blocked(
json=ro.ap_object, json=ro.ap_object,
) )
# Then the server returns a 204 # Then the server returns a 202
assert response.status_code == 202 assert response.status_code == 202
# And when processing the incoming activity from a blocked actor # And when processing the incoming activity from a blocked actor
run_async(_process_next_incoming_activity) run_process_next_incoming_activity()
# Then the Create activity was discarded # Then the Create activity was discarded
assert ( assert (
@ -389,10 +380,10 @@ def test_inbox__move_activity(
json=move_activity.ap_object, json=move_activity.ap_object,
) )
# Then the server returns a 204 # Then the server returns a 202
assert response.status_code == 202 assert response.status_code == 202
run_async(_process_next_incoming_activity) run_process_next_incoming_activity()
# And the Move activity was saved in the inbox # And the Move activity was saved in the inbox
inbox_activity = db.execute(select(models.InboxObject)).scalar_one() inbox_activity = db.execute(select(models.InboxObject)).scalar_one()

View file

@ -0,0 +1,109 @@
import httpx
import respx
from fastapi.testclient import TestClient
from sqlalchemy import func
from sqlalchemy import select
from sqlalchemy.orm import Session
from app import activitypub as ap
from app import models
from app.ap_object import RemoteObject
from tests import factories
from tests.utils import mock_httpsig_checker
from tests.utils import run_process_next_incoming_activity
from tests.utils import setup_remote_actor
from tests.utils import setup_remote_actor_as_following_and_follower
def test_inbox__incoming_delete_for_unknown_actor(
db: Session,
client: TestClient,
respx_mock: respx.MockRouter,
) -> None:
# Given a remote actor who is already deleted
ra = factories.RemoteActorFactory(
base_url="https://deleted.com",
username="toto",
public_key="pk",
)
respx_mock.get(ra.ap_id).mock(return_value=httpx.Response(404, json=ra.ap_actor))
# When receiving a Delete activity for an unknown actor
delete_activity = RemoteObject(
factories.build_delete_activity(
from_remote_actor=ra,
deleted_object_ap_id=ra.ap_id,
),
ra,
)
with mock_httpsig_checker(ra, has_valid_signature=False, is_ap_actor_gone=True):
response = client.post(
"/inbox",
headers={"Content-Type": ap.AS_CTX},
json=delete_activity.ap_object,
)
# Then the server returns a 202
assert response.status_code == 202
# And no incoming activity was created
assert db.scalar(select(func.count(models.IncomingActivity.id))) == 0
def test_inbox__incoming_delete_for_known_actor(
db: Session,
client: TestClient,
respx_mock: respx.MockRouter,
) -> None:
# Given a remote actor
ra = setup_remote_actor(respx_mock)
# Which is both followed and a follower
following, _ = setup_remote_actor_as_following_and_follower(ra)
actor = following.actor
assert actor
assert following.outbox_object
# TODO: setup few more activities (like announce and create)
# When receiving a Delete activity for an unknown actor
delete_activity = RemoteObject(
factories.build_delete_activity(
from_remote_actor=ra,
deleted_object_ap_id=ra.ap_id,
),
ra,
)
with mock_httpsig_checker(ra):
response = client.post(
"/inbox",
headers={"Content-Type": ap.AS_CTX},
json=delete_activity.ap_object,
)
# Then the server returns a 202
assert response.status_code == 202
run_process_next_incoming_activity()
# Then every inbox object from the actor was deleted
assert (
db.scalar(
select(func.count(models.InboxObject.id)).where(
models.InboxObject.actor_id == actor.id,
models.InboxObject.is_deleted.is_(False),
)
)
== 0
)
# And the following actor was deleted
assert db.scalar(select(func.count(models.Following.id))) == 0
# And the follower actor was deleted too
assert db.scalar(select(func.count(models.Follower.id))) == 0
# And the actor was marked in deleted
db.refresh(actor)
assert actor.is_deleted is True

View file

@ -14,19 +14,27 @@ from app import models
from app.actor import LOCAL_ACTOR from app.actor import LOCAL_ACTOR
from app.ap_object import RemoteObject from app.ap_object import RemoteObject
from app.config import session_serializer from app.config import session_serializer
from app.database import AsyncSession
from app.database import async_session from app.database import async_session
from app.incoming_activities import fetch_next_incoming_activity
from app.incoming_activities import process_next_incoming_activity
from app.main import app from app.main import app
from tests import factories from tests import factories
@contextmanager @contextmanager
def mock_httpsig_checker(ra: actor.RemoteActor): def mock_httpsig_checker(
ra: actor.RemoteActor,
has_valid_signature: bool = True,
is_ap_actor_gone: bool = False,
):
async def httpsig_checker( async def httpsig_checker(
request: fastapi.Request, request: fastapi.Request,
) -> httpsig.HTTPSigInfo: ) -> httpsig.HTTPSigInfo:
return httpsig.HTTPSigInfo( return httpsig.HTTPSigInfo(
has_valid_signature=True, has_valid_signature=has_valid_signature,
signed_by_ap_actor_id=ra.ap_id, signed_by_ap_actor_id=ra.ap_id,
is_ap_actor_gone=is_ap_actor_gone,
) )
app.dependency_overrides[httpsig.httpsig_checker] = httpsig_checker app.dependency_overrides[httpsig.httpsig_checker] = httpsig_checker
@ -115,6 +123,52 @@ def setup_remote_actor_as_following(ra: actor.RemoteActor) -> models.Following:
return following return following
def setup_remote_actor_as_following_and_follower(
ra: actor.RemoteActor,
) -> tuple[models.Following, models.Follower]:
actor = factories.ActorFactory.from_remote_actor(ra)
follow_id = uuid4().hex
follow_from_outbox = RemoteObject(
factories.build_follow_activity(
from_remote_actor=LOCAL_ACTOR,
for_remote_actor=ra,
outbox_public_id=follow_id,
),
LOCAL_ACTOR,
)
outbox_object = factories.OutboxObjectFactory.from_remote_object(
follow_id, follow_from_outbox
)
following = factories.FollowingFactory(
outbox_object_id=outbox_object.id,
actor_id=actor.id,
ap_actor_id=actor.ap_id,
)
follow_id = uuid4().hex
follow_from_inbox = RemoteObject(
factories.build_follow_activity(
from_remote_actor=ra,
for_remote_actor=LOCAL_ACTOR,
outbox_public_id=follow_id,
),
ra,
)
inbox_object = factories.InboxObjectFactory.from_remote_object(
follow_from_inbox, actor
)
follower = factories.FollowerFactory(
inbox_object_id=inbox_object.id,
actor_id=actor.id,
ap_actor_id=actor.ap_id,
)
return following, follower
def setup_inbox_delete( def setup_inbox_delete(
actor: models.Actor, deleted_object_ap_id: str actor: models.Actor, deleted_object_ap_id: str
) -> models.InboxObject: ) -> models.InboxObject:
@ -137,3 +191,13 @@ def run_async(func, *args, **kwargs):
return await func(db, *args, **kwargs) return await func(db, *args, **kwargs)
asyncio.run(_func()) asyncio.run(_func())
async def _process_next_incoming_activity(db_session: AsyncSession) -> None:
next_activity = await fetch_next_incoming_activity(db_session)
assert next_activity
await process_next_incoming_activity(db_session, next_activity)
def run_process_next_incoming_activity() -> None:
run_async(_process_next_incoming_activity)