mirror of
				https://git.sr.ht/~tsileo/microblog.pub
				synced 2025-06-05 21:59:23 +02:00 
			
		
		
		
	Add test cases for remote actor deletion
This commit is contained in:
		@@ -12,24 +12,15 @@ from app import activitypub as ap
 | 
			
		||||
from app import models
 | 
			
		||||
from app.actor import LOCAL_ACTOR
 | 
			
		||||
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.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_remote_actor
 | 
			
		||||
from tests.utils import setup_remote_actor_as_follower
 | 
			
		||||
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(
 | 
			
		||||
    client: TestClient,
 | 
			
		||||
):
 | 
			
		||||
@@ -70,10 +61,10 @@ def test_inbox_incoming_follow_request(
 | 
			
		||||
            json=follow_activity.ap_object,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    # Then the server returns a 204
 | 
			
		||||
    # Then the server returns a 202
 | 
			
		||||
    assert response.status_code == 202
 | 
			
		||||
 | 
			
		||||
    run_async(_process_next_incoming_activity)
 | 
			
		||||
    run_process_next_incoming_activity()
 | 
			
		||||
 | 
			
		||||
    # And the actor was saved in DB
 | 
			
		||||
    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,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    # Then the server returns a 204
 | 
			
		||||
    # Then the server returns a 202
 | 
			
		||||
    assert response.status_code == 202
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
    saved_actor = db.execute(select(models.Actor)).scalar_one()
 | 
			
		||||
@@ -183,10 +174,10 @@ def test_inbox_accept_follow_request(
 | 
			
		||||
            json=accept_activity.ap_object,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    # Then the server returns a 204
 | 
			
		||||
    # Then the server returns a 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
 | 
			
		||||
    inbox_activity = db.execute(select(models.InboxObject)).scalar_one()
 | 
			
		||||
@@ -229,11 +220,11 @@ def test_inbox__create_from_follower(
 | 
			
		||||
            json=ro.ap_object,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    # Then the server returns a 204
 | 
			
		||||
    # Then the server returns a 202
 | 
			
		||||
    assert response.status_code == 202
 | 
			
		||||
 | 
			
		||||
    # And when processing the incoming activity
 | 
			
		||||
    run_async(_process_next_incoming_activity)
 | 
			
		||||
    run_process_next_incoming_activity()
 | 
			
		||||
 | 
			
		||||
    # Then the Create activity was saved
 | 
			
		||||
    create_activity_from_inbox: models.InboxObject | None = db.execute(
 | 
			
		||||
@@ -283,11 +274,11 @@ def test_inbox__create_already_deleted_object(
 | 
			
		||||
            json=ro.ap_object,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    # Then the server returns a 204
 | 
			
		||||
    # Then the server returns a 202
 | 
			
		||||
    assert response.status_code == 202
 | 
			
		||||
 | 
			
		||||
    # And when processing the incoming activity
 | 
			
		||||
    run_async(_process_next_incoming_activity)
 | 
			
		||||
    run_process_next_incoming_activity()
 | 
			
		||||
 | 
			
		||||
    # Then the Create activity was saved
 | 
			
		||||
    create_activity_from_inbox: models.InboxObject | None = db.execute(
 | 
			
		||||
@@ -339,11 +330,11 @@ def test_inbox__actor_is_blocked(
 | 
			
		||||
            json=ro.ap_object,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    # Then the server returns a 204
 | 
			
		||||
    # Then the server returns a 202
 | 
			
		||||
    assert response.status_code == 202
 | 
			
		||||
 | 
			
		||||
    # 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
 | 
			
		||||
    assert (
 | 
			
		||||
@@ -389,10 +380,10 @@ def test_inbox__move_activity(
 | 
			
		||||
            json=move_activity.ap_object,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    # Then the server returns a 204
 | 
			
		||||
    # Then the server returns a 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
 | 
			
		||||
    inbox_activity = db.execute(select(models.InboxObject)).scalar_one()
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										109
									
								
								tests/test_remote_actor_deletion.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								tests/test_remote_actor_deletion.py
									
									
									
									
									
										Normal 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
 | 
			
		||||
@@ -14,19 +14,27 @@ from app import models
 | 
			
		||||
from app.actor import LOCAL_ACTOR
 | 
			
		||||
from app.ap_object import RemoteObject
 | 
			
		||||
from app.config import session_serializer
 | 
			
		||||
from app.database import AsyncSession
 | 
			
		||||
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 tests import factories
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@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(
 | 
			
		||||
        request: fastapi.Request,
 | 
			
		||||
    ) -> httpsig.HTTPSigInfo:
 | 
			
		||||
        return httpsig.HTTPSigInfo(
 | 
			
		||||
            has_valid_signature=True,
 | 
			
		||||
            has_valid_signature=has_valid_signature,
 | 
			
		||||
            signed_by_ap_actor_id=ra.ap_id,
 | 
			
		||||
            is_ap_actor_gone=is_ap_actor_gone,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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(
 | 
			
		||||
    actor: models.Actor, deleted_object_ap_id: str
 | 
			
		||||
) -> models.InboxObject:
 | 
			
		||||
@@ -137,3 +191,13 @@ def run_async(func, *args, **kwargs):
 | 
			
		||||
            return await func(db, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    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)
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user