from unittest import mock import respx from fastapi.testclient import TestClient from sqlalchemy import select from sqlalchemy.orm import Session from app import activitypub as ap from app import models from app import webfinger from app.actor import LOCAL_ACTOR from app.config import generate_csrf_token from tests.utils import generate_admin_session_cookies from tests.utils import setup_inbox_note from tests.utils import setup_outbox_note from tests.utils import setup_remote_actor from tests.utils import setup_remote_actor_as_follower def test_outbox__no_activities( db: Session, client: TestClient, ) -> None: response = client.get("/outbox", headers={"Accept": ap.AP_CONTENT_TYPE}) assert response.status_code == 200 json_response = response.json() assert json_response["totalItems"] == 0 assert json_response["orderedItems"] == [] def test_send_follow_request( db: Session, client: TestClient, respx_mock: respx.MockRouter, ) -> None: # given a remote actor ra = setup_remote_actor(respx_mock) response = client.post( "/admin/actions/follow", data={ "redirect_url": "http://testserver/", "ap_actor_id": ra.ap_id, "csrf_token": generate_csrf_token(), }, cookies=generate_admin_session_cookies(), ) # Then the server returns a 302 assert response.status_code == 302 assert response.headers.get("Location") == "http://testserver/" # And the Follow activity was created in the outbox outbox_object = db.execute(select(models.OutboxObject)).scalar_one() assert outbox_object.ap_type == "Follow" assert outbox_object.activity_object_ap_id == ra.ap_id # And an outgoing activity was queued outgoing_activity = db.execute(select(models.OutgoingActivity)).scalar_one() assert outgoing_activity.outbox_object_id == outbox_object.id assert outgoing_activity.recipient == ra.inbox_url def test_send_delete__reverts_side_effects( db: Session, client: TestClient, respx_mock: respx.MockRouter, ) -> None: # given a remote actor ra = setup_remote_actor(respx_mock) # who is a follower follower = setup_remote_actor_as_follower(ra) actor = follower.actor # with a note that has existing replies inbox_note = setup_inbox_note(actor) # with a bogus counter inbox_note.replies_count = 5 db.commit() # and 2 local replies setup_outbox_note( to=[ap.AS_PUBLIC], cc=[LOCAL_ACTOR.followers_collection_id], # type: ignore in_reply_to=inbox_note.ap_id, ) outbox_note2 = setup_outbox_note( to=[ap.AS_PUBLIC], cc=[LOCAL_ACTOR.followers_collection_id], # type: ignore in_reply_to=inbox_note.ap_id, ) db.commit() # When deleting one of the replies response = client.post( "/admin/actions/delete", data={ "redirect_url": "http://testserver/", "ap_object_id": outbox_note2.ap_id, "csrf_token": generate_csrf_token(), }, cookies=generate_admin_session_cookies(), ) # Then the server returns a 302 assert response.status_code == 302 assert response.headers.get("Location") == "http://testserver/" # And the Delete activity was created in the outbox outbox_object = db.execute( select(models.OutboxObject).where(models.OutboxObject.ap_type == "Delete") ).scalar_one() assert outbox_object.ap_type == "Delete" assert outbox_object.activity_object_ap_id == outbox_note2.ap_id # And an outgoing activity was queued outgoing_activity = db.execute(select(models.OutgoingActivity)).scalar_one() assert outgoing_activity.outbox_object_id == outbox_object.id assert outgoing_activity.recipient == ra.inbox_url # And the replies count of the replied object was refreshed correctly db.refresh(inbox_note) assert inbox_note.replies_count == 1 def test_send_create_activity__no_content( db: Session, client: TestClient, respx_mock: respx.MockRouter, ) -> None: # given a remote actor ra = setup_remote_actor(respx_mock) with mock.patch.object(webfinger, "get_actor_url", return_value=ra.ap_id): response = client.post( "/admin/actions/new", data={ "redirect_url": "http://testserver/", "visibility": ap.VisibilityEnum.PUBLIC.name, "csrf_token": generate_csrf_token(), }, cookies=generate_admin_session_cookies(), ) # Then the server returns a 422 assert response.status_code == 422 def test_send_create_activity__with_attachment( db: Session, client: TestClient, respx_mock: respx.MockRouter, ) -> None: # given a remote actor ra = setup_remote_actor(respx_mock) with mock.patch.object(webfinger, "get_actor_url", return_value=ra.ap_id): response = client.post( "/admin/actions/new", data={ "content": "hello", "redirect_url": "http://testserver/", "visibility": ap.VisibilityEnum.PUBLIC.name, "csrf_token": generate_csrf_token(), }, files=[ ("files", ("attachment.txt", "hello")), ], cookies=generate_admin_session_cookies(), ) # Then the server returns a 302 assert response.status_code == 302 # And the Follow activity was created in the outbox outbox_object = db.execute(select(models.OutboxObject)).scalar_one() assert outbox_object.ap_type == "Note" assert outbox_object.summary is None assert outbox_object.content == "

hello

\n" assert len(outbox_object.attachments) == 1 attachment = outbox_object.attachments[0] assert attachment.type == "Document" attachment_response = client.get(attachment.url) assert attachment_response.status_code == 200 assert attachment_response.content == b"hello" upload = db.execute(select(models.Upload)).scalar_one() assert upload.content_hash == ( "324dcf027dd4a30a932c441f365a25e86b173defa4b8e58948253471b81b72cf" ) outbox_attachment = db.execute(select(models.OutboxObjectAttachment)).scalar_one() assert outbox_attachment.upload_id == upload.id assert outbox_attachment.outbox_object_id == outbox_object.id assert outbox_attachment.filename == "attachment.txt" def test_send_create_activity__no_content_with_cw_and_attachments( db: Session, client: TestClient, respx_mock: respx.MockRouter, ) -> None: # given a remote actor ra = setup_remote_actor(respx_mock) with mock.patch.object(webfinger, "get_actor_url", return_value=ra.ap_id): response = client.post( "/admin/actions/new", data={ "content_warning": "cw", "redirect_url": "http://testserver/", "visibility": ap.VisibilityEnum.PUBLIC.name, "csrf_token": generate_csrf_token(), }, files={"files": ("attachment.txt", "hello")}, cookies=generate_admin_session_cookies(), ) # Then the server returns a 302 assert response.status_code == 302 # And the Follow activity was created in the outbox outbox_object = db.execute(select(models.OutboxObject)).scalar_one() assert outbox_object.ap_type == "Note" assert outbox_object.summary is None assert outbox_object.content == "

cw

\n" assert len(outbox_object.attachments) == 1 def test_send_create_activity__no_followers_and_with_mention( db: Session, client: TestClient, respx_mock: respx.MockRouter, ) -> None: # given a remote actor ra = setup_remote_actor(respx_mock) with mock.patch.object(webfinger, "get_actor_url", return_value=ra.ap_id): response = client.post( "/admin/actions/new", data={ "redirect_url": "http://testserver/", "content": "hi @toto@example.com", "visibility": ap.VisibilityEnum.PUBLIC.name, "csrf_token": generate_csrf_token(), }, cookies=generate_admin_session_cookies(), ) # Then the server returns a 302 assert response.status_code == 302 # And the Follow activity was created in the outbox outbox_object = db.execute(select(models.OutboxObject)).scalar_one() assert outbox_object.ap_type == "Note" # And an outgoing activity was queued outgoing_activity = db.execute(select(models.OutgoingActivity)).scalar_one() assert outgoing_activity.outbox_object_id == outbox_object.id assert outgoing_activity.recipient == ra.inbox_url def test_send_create_activity__with_followers( db: Session, client: TestClient, respx_mock: respx.MockRouter, ) -> None: # given a remote actor ra = setup_remote_actor(respx_mock) # who is a follower follower = setup_remote_actor_as_follower(ra) with mock.patch.object(webfinger, "get_actor_url", return_value=ra.ap_id): response = client.post( "/admin/actions/new", data={ "redirect_url": "http://testserver/", "content": "hi followers", "visibility": ap.VisibilityEnum.PUBLIC.name, "csrf_token": generate_csrf_token(), }, cookies=generate_admin_session_cookies(), ) # Then the server returns a 302 assert response.status_code == 302 # And the Follow activity was created in the outbox outbox_object = db.execute(select(models.OutboxObject)).scalar_one() assert outbox_object.ap_type == "Note" # And an outgoing activity was queued outgoing_activity = db.execute(select(models.OutgoingActivity)).scalar_one() assert outgoing_activity.outbox_object_id == outbox_object.id assert outgoing_activity.recipient == follower.actor.inbox_url def test_send_create_activity__question__one_of( db: Session, client: TestClient, respx_mock: respx.MockRouter, ) -> None: # given a remote actor ra = setup_remote_actor(respx_mock) # who is a follower follower = setup_remote_actor_as_follower(ra) with mock.patch.object(webfinger, "get_actor_url", return_value=ra.ap_id): response = client.post( "/admin/actions/new", data={ "redirect_url": "http://testserver/", "content": "hi followers", "visibility": ap.VisibilityEnum.PUBLIC.name, "csrf_token": generate_csrf_token(), "poll_type": "oneOf", "poll_duration": 5, "poll_answer_1": "A", "poll_answer_2": "B", }, cookies=generate_admin_session_cookies(), ) # Then the server returns a 302 assert response.status_code == 302 # And the Follow activity was created in the outbox outbox_object = db.execute(select(models.OutboxObject)).scalar_one() assert outbox_object.ap_type == "Question" assert outbox_object.is_one_of_poll is True assert len(outbox_object.poll_items) == 2 assert {pi["name"] for pi in outbox_object.poll_items} == {"A", "B"} assert outbox_object.is_poll_ended is False # And an outgoing activity was queued outgoing_activity = db.execute(select(models.OutgoingActivity)).scalar_one() assert outgoing_activity.outbox_object_id == outbox_object.id assert outgoing_activity.recipient == follower.actor.inbox_url def test_send_create_activity__question__any_of( db: Session, client: TestClient, respx_mock: respx.MockRouter, ) -> None: # given a remote actor ra = setup_remote_actor(respx_mock) # who is a follower follower = setup_remote_actor_as_follower(ra) with mock.patch.object(webfinger, "get_actor_url", return_value=ra.ap_id): response = client.post( "/admin/actions/new", data={ "redirect_url": "http://testserver/", "content": "hi followers", "visibility": ap.VisibilityEnum.PUBLIC.name, "csrf_token": generate_csrf_token(), "poll_type": "anyOf", "poll_duration": 10, "poll_answer_1": "A", "poll_answer_2": "B", "poll_answer_3": "C", "poll_answer_4": "D", }, cookies=generate_admin_session_cookies(), ) # Then the server returns a 302 assert response.status_code == 302 # And the Follow activity was created in the outbox outbox_object = db.execute(select(models.OutboxObject)).scalar_one() assert outbox_object.ap_type == "Question" assert outbox_object.is_one_of_poll is False assert len(outbox_object.poll_items) == 4 assert {pi["name"] for pi in outbox_object.poll_items} == {"A", "B", "C", "D"} assert outbox_object.is_poll_ended is False # And an outgoing activity was queued outgoing_activity = db.execute(select(models.OutgoingActivity)).scalar_one() assert outgoing_activity.outbox_object_id == outbox_object.id assert outgoing_activity.recipient == follower.actor.inbox_url def test_send_create_activity__article( db: Session, client: TestClient, respx_mock: respx.MockRouter, ) -> None: # given a remote actor ra = setup_remote_actor(respx_mock) # who is a follower follower = setup_remote_actor_as_follower(ra) with mock.patch.object(webfinger, "get_actor_url", return_value=ra.ap_id): response = client.post( "/admin/actions/new", data={ "redirect_url": "http://testserver/", "content": "hi followers", "visibility": ap.VisibilityEnum.PUBLIC.name, "csrf_token": generate_csrf_token(), "name": "Article", }, cookies=generate_admin_session_cookies(), ) # Then the server returns a 302 assert response.status_code == 302 # And the Follow activity was created in the outbox outbox_object = db.execute(select(models.OutboxObject)).scalar_one() assert outbox_object.ap_type == "Article" assert outbox_object.ap_object["name"] == "Article" # And an outgoing activity was queued outgoing_activity = db.execute(select(models.OutgoingActivity)).scalar_one() assert outgoing_activity.outbox_object_id == outbox_object.id assert outgoing_activity.recipient == follower.actor.inbox_url