microblog.pub/tests/test_inbox.py

476 lines
14 KiB
Python
Raw Permalink Normal View History

from unittest import mock
2022-06-22 20:11:22 +02:00
from uuid import uuid4
import httpx
import respx
from fastapi.testclient import TestClient
2022-07-31 10:35:11 +02:00
from sqlalchemy import func
2022-07-26 21:10:59 +02:00
from sqlalchemy import select
2022-06-29 20:43:17 +02:00
from sqlalchemy.orm import Session
2022-06-22 20:11:22 +02:00
from app import activitypub as ap
from app import models
from app.actor import LOCAL_ACTOR
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
2022-07-26 21:10:59 +02:00
from tests.utils import setup_inbox_delete
2022-07-26 20:26:34 +02:00
from tests.utils import setup_remote_actor
2022-07-26 21:10:59 +02:00
from tests.utils import setup_remote_actor_as_follower
2022-08-16 22:15:05 +02:00
from tests.utils import setup_remote_actor_as_following
2022-06-22 20:11:22 +02:00
def test_inbox_requires_httpsig(
client: TestClient,
):
response = client.post(
"/inbox",
headers={"Content-Type": ap.AS_CTX},
json={},
)
assert response.status_code == 401
assert response.json()["detail"] == "Invalid HTTP sig"
def test_inbox_incoming_follow_request(
2022-06-22 20:11:22 +02:00
db: Session,
client: TestClient,
respx_mock: respx.MockRouter,
) -> None:
# Given a remote actor
ra = factories.RemoteActorFactory(
base_url="https://example.com",
username="toto",
public_key="pk",
)
respx_mock.get(ra.ap_id).mock(return_value=httpx.Response(200, json=ra.ap_actor))
2022-07-26 21:10:59 +02:00
# When receiving a Follow activity
2022-06-22 20:11:22 +02:00
follow_activity = RemoteObject(
factories.build_follow_activity(
from_remote_actor=ra,
for_remote_actor=LOCAL_ACTOR,
2022-06-30 00:28:07 +02:00
),
ra,
2022-06-22 20:11:22 +02:00
)
with mock_httpsig_checker(ra):
response = client.post(
"/inbox",
headers={"Content-Type": ap.AS_CTX},
json=follow_activity.ap_object,
)
# Then the server returns a 202
2022-07-14 08:44:04 +02:00
assert response.status_code == 202
run_process_next_incoming_activity()
2022-06-22 20:11:22 +02:00
# And the actor was saved in DB
saved_actor = db.execute(select(models.Actor)).scalar_one()
2022-06-22 20:11:22 +02:00
assert saved_actor.ap_id == ra.ap_id
# And the Follow activity was saved in the inbox
inbox_object = db.execute(select(models.InboxObject)).scalar_one()
2022-06-22 20:11:22 +02:00
assert inbox_object.ap_object == follow_activity.ap_object
# And a follower was internally created
2022-09-02 23:47:23 +02:00
follower = db.execute(select(models.Follower)).scalar_one()
2022-06-22 20:11:22 +02:00
assert follower.ap_actor_id == ra.ap_id
assert follower.actor_id == saved_actor.id
assert follower.inbox_object_id == inbox_object.id
# And an Accept activity was created in the outbox
outbox_object = db.execute(select(models.OutboxObject)).scalar_one()
2022-06-22 20:11:22 +02:00
assert outbox_object.ap_type == "Accept"
assert outbox_object.activity_object_ap_id == follow_activity.ap_id
# And an outgoing activity was created to track the Accept activity delivery
outgoing_activity = db.execute(select(models.OutgoingActivity)).scalar_one()
2022-06-22 20:11:22 +02:00
assert outgoing_activity.outbox_object_id == outbox_object.id
def test_inbox_incoming_follow_request__manually_approves_followers(
db: Session,
client: TestClient,
respx_mock: respx.MockRouter,
) -> None:
# Given a remote actor
ra = factories.RemoteActorFactory(
base_url="https://example.com",
username="toto",
public_key="pk",
)
respx_mock.get(ra.ap_id).mock(return_value=httpx.Response(200, json=ra.ap_actor))
# When receiving a Follow activity
follow_activity = RemoteObject(
factories.build_follow_activity(
from_remote_actor=ra,
for_remote_actor=LOCAL_ACTOR,
),
ra,
)
with mock_httpsig_checker(ra):
response = client.post(
"/inbox",
headers={"Content-Type": ap.AS_CTX},
json=follow_activity.ap_object,
)
# Then the server returns a 202
assert response.status_code == 202
with mock.patch("app.boxes.MANUALLY_APPROVES_FOLLOWERS", True):
run_process_next_incoming_activity()
# And the actor was saved in DB
saved_actor = db.execute(select(models.Actor)).scalar_one()
assert saved_actor.ap_id == ra.ap_id
# And the Follow activity was saved in the inbox
inbox_object = db.execute(select(models.InboxObject)).scalar_one()
assert inbox_object.ap_object == follow_activity.ap_object
# And no follower was internally created
assert db.scalar(select(func.count(models.Follower.id))) == 0
2022-06-22 20:11:22 +02:00
def test_inbox_accept_follow_request(
db: Session,
client: TestClient,
respx_mock: respx.MockRouter,
) -> None:
# Given a remote actor
2022-07-26 20:26:34 +02:00
ra = setup_remote_actor(respx_mock)
2022-06-22 20:11:22 +02:00
actor_in_db = factories.ActorFactory.from_remote_actor(ra)
# And a Follow activity in the outbox
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,
2022-06-30 00:28:07 +02:00
),
LOCAL_ACTOR,
2022-06-22 20:11:22 +02:00
)
outbox_object = factories.OutboxObjectFactory.from_remote_object(
follow_id, follow_from_outbox
)
2022-07-26 21:10:59 +02:00
# When receiving a Accept activity
2022-06-22 20:11:22 +02:00
accept_activity = RemoteObject(
factories.build_accept_activity(
from_remote_actor=ra,
for_remote_object=follow_from_outbox,
2022-06-30 00:28:07 +02:00
),
ra,
2022-06-22 20:11:22 +02:00
)
with mock_httpsig_checker(ra):
response = client.post(
"/inbox",
headers={"Content-Type": ap.AS_CTX},
json=accept_activity.ap_object,
)
# Then the server returns a 202
2022-07-14 08:44:04 +02:00
assert response.status_code == 202
run_process_next_incoming_activity()
2022-06-22 20:11:22 +02:00
# And the Accept activity was saved in the inbox
inbox_activity = db.execute(select(models.InboxObject)).scalar_one()
2022-06-22 20:11:22 +02:00
assert inbox_activity.ap_type == "Accept"
assert inbox_activity.relates_to_outbox_object_id == outbox_object.id
assert inbox_activity.actor_id == actor_in_db.id
# And a following entry was created internally
following = db.execute(select(models.Following)).scalar_one()
2022-06-22 20:11:22 +02:00
assert following.ap_actor_id == actor_in_db.ap_id
2022-07-26 21:10:59 +02:00
def test_inbox__create_from_follower(
db: Session,
client: TestClient,
respx_mock: respx.MockRouter,
) -> None:
# Given a remote actor
ra = setup_remote_actor(respx_mock)
# Who is also a follower
setup_remote_actor_as_follower(ra)
create_activity = factories.build_create_activity(
factories.build_note_object(
from_remote_actor=ra,
outbox_public_id=str(uuid4()),
content="Hello",
to=[LOCAL_ACTOR.ap_id],
)
)
# When receiving a Create activity
ro = RemoteObject(create_activity, ra)
with mock_httpsig_checker(ra):
response = client.post(
"/inbox",
headers={"Content-Type": ap.AS_CTX},
json=ro.ap_object,
)
# Then the server returns a 202
2022-07-26 21:10:59 +02:00
assert response.status_code == 202
# And when processing the incoming activity
run_process_next_incoming_activity()
2022-07-26 21:10:59 +02:00
# Then the Create activity was saved
create_activity_from_inbox: models.InboxObject | None = db.execute(
select(models.InboxObject).where(models.InboxObject.ap_type == "Create")
).scalar_one_or_none()
assert create_activity_from_inbox
assert create_activity_from_inbox.ap_id == ro.ap_id
# And the Note object was created
note_activity_from_inbox: models.InboxObject | None = db.execute(
select(models.InboxObject).where(models.InboxObject.ap_type == "Note")
).scalar_one_or_none()
assert note_activity_from_inbox
assert note_activity_from_inbox.ap_id == ro.activity_object_ap_id
def test_inbox__create_already_deleted_object(
db: Session,
client: TestClient,
respx_mock: respx.MockRouter,
) -> None:
# Given a remote actor
ra = setup_remote_actor(respx_mock)
# Who is also a follower
follower = setup_remote_actor_as_follower(ra)
# And a Create activity for a Note object
create_activity = factories.build_create_activity(
factories.build_note_object(
from_remote_actor=ra,
outbox_public_id=str(uuid4()),
content="Hello",
to=[LOCAL_ACTOR.ap_id],
)
)
ro = RemoteObject(create_activity, ra)
# And a Delete activity received for the create object
setup_inbox_delete(follower.actor, ro.activity_object_ap_id) # type: ignore
# When receiving a Create activity
with mock_httpsig_checker(ra):
response = client.post(
"/inbox",
headers={"Content-Type": ap.AS_CTX},
json=ro.ap_object,
)
# Then the server returns a 202
2022-07-26 21:10:59 +02:00
assert response.status_code == 202
# And when processing the incoming activity
run_process_next_incoming_activity()
2022-07-26 21:10:59 +02:00
# Then the Create activity was saved
create_activity_from_inbox: models.InboxObject | None = db.execute(
select(models.InboxObject).where(models.InboxObject.ap_type == "Create")
).scalar_one_or_none()
assert create_activity_from_inbox
assert create_activity_from_inbox.ap_id == ro.ap_id
# But it has the deleted flag
assert create_activity_from_inbox.is_deleted is True
# And the Note wasn't created
assert (
db.execute(
select(models.InboxObject).where(models.InboxObject.ap_type == "Note")
).scalar_one_or_none()
is None
)
2022-07-31 10:35:11 +02:00
def test_inbox__actor_is_blocked(
db: Session,
client: TestClient,
respx_mock: respx.MockRouter,
) -> None:
# Given a remote actor
ra = setup_remote_actor(respx_mock)
# Who is also a follower
follower = setup_remote_actor_as_follower(ra)
follower.actor.is_blocked = True
db.commit()
create_activity = factories.build_create_activity(
factories.build_note_object(
from_remote_actor=ra,
outbox_public_id=str(uuid4()),
content="Hello",
to=[LOCAL_ACTOR.ap_id],
)
)
# When receiving a Create activity
ro = RemoteObject(create_activity, ra)
with mock_httpsig_checker(ra):
response = client.post(
"/inbox",
headers={"Content-Type": ap.AS_CTX},
json=ro.ap_object,
)
# Then the server returns a 202
2022-07-31 10:35:11 +02:00
assert response.status_code == 202
# And when processing the incoming activity from a blocked actor
run_process_next_incoming_activity()
2022-07-31 10:35:11 +02:00
# Then the Create activity was discarded
assert (
db.scalar(
select(func.count(models.InboxObject.id)).where(
models.InboxObject.ap_type != "Follow"
)
)
== 0
)
2022-08-16 22:15:05 +02:00
def test_inbox__move_activity(
db: Session,
client: TestClient,
respx_mock: respx.MockRouter,
) -> None:
# Given a remote actor
ra = setup_remote_actor(respx_mock)
# Which is followed by the local actor
following = setup_remote_actor_as_following(ra)
old_actor = following.actor
assert old_actor
assert following.outbox_object
follow_id = following.outbox_object.ap_id
# When receiving a Move activity
new_ra = setup_remote_actor(
respx_mock,
base_url="https://new-account.com",
also_known_as=[ra.ap_id],
)
move_activity = RemoteObject(
factories.build_move_activity(ra, new_ra),
ra,
)
with mock_httpsig_checker(ra):
response = client.post(
"/inbox",
headers={"Content-Type": ap.AS_CTX},
json=move_activity.ap_object,
)
# Then the server returns a 202
2022-08-16 22:15:05 +02:00
assert response.status_code == 202
run_process_next_incoming_activity()
2022-08-16 22:15:05 +02:00
# And the Move activity was saved in the inbox
inbox_activity = db.execute(select(models.InboxObject)).scalar_one()
assert inbox_activity.ap_type == "Move"
assert inbox_activity.actor_id == old_actor.id
# And the following actor was deleted
assert db.scalar(select(func.count(models.Following.id))) == 0
# And the follow was undone
assert (
db.scalar(
select(func.count(models.OutboxObject.id)).where(
models.OutboxObject.ap_type == "Undo",
models.OutboxObject.activity_object_ap_id == follow_id,
)
)
== 1
)
# And the new account was followed
assert (
db.scalar(
select(func.count(models.OutboxObject.id)).where(
models.OutboxObject.ap_type == "Follow",
models.OutboxObject.activity_object_ap_id == new_ra.ap_id,
)
)
== 1
)
# And a notification was created
notif = db.execute(
select(models.Notification).where(
models.Notification.notification_type == models.NotificationType.MOVE
)
).scalar_one()
assert notif.actor.ap_id == new_ra.ap_id
assert notif.inbox_object_id == inbox_activity.id
2022-10-18 21:39:09 +02:00
def test_inbox__block_activity(
db: Session,
client: TestClient,
respx_mock: respx.MockRouter,
) -> None:
# Given a remote actor
ra = setup_remote_actor(respx_mock)
# Which is followed by the local actor
setup_remote_actor_as_following(ra)
# When receiving a Block activity
follow_activity = RemoteObject(
factories.build_block_activity(
from_remote_actor=ra,
for_remote_actor=LOCAL_ACTOR,
),
ra,
)
with mock_httpsig_checker(ra):
response = client.post(
"/inbox",
headers={"Content-Type": ap.AS_CTX},
json=follow_activity.ap_object,
)
# Then the server returns a 202
assert response.status_code == 202
run_process_next_incoming_activity()
# And the actor was saved in DB
saved_actor = db.execute(select(models.Actor)).scalar_one()
assert saved_actor.ap_id == ra.ap_id
# And the Block activity was saved in the inbox
inbox_activity = db.execute(
select(models.InboxObject).where(models.InboxObject.ap_type == "Block")
).scalar_one()
# And a notification was created
notif = db.execute(
select(models.Notification).where(
models.Notification.notification_type == models.NotificationType.BLOCKED
)
).scalar_one()
assert notif.actor.ap_id == ra.ap_id
assert notif.inbox_object_id == inbox_activity.id