mirror of
https://github.com/jointakahe/takahe.git
synced 2024-11-25 16:51:00 +00:00
parent
d525a2f465
commit
716b74404f
5 changed files with 90 additions and 2 deletions
|
@ -106,13 +106,15 @@ def domain2() -> Domain:
|
|||
|
||||
@pytest.fixture
|
||||
@pytest.mark.django_db
|
||||
def identity(user, domain) -> Identity:
|
||||
def identity(user, domain, keypair) -> Identity:
|
||||
"""
|
||||
Creates a basic test identity with a user and domain.
|
||||
"""
|
||||
identity = Identity.objects.create(
|
||||
actor_uri="https://example.com/@test@example.com/",
|
||||
inbox_uri="https://example.com/@test@example.com/inbox/",
|
||||
private_key=keypair["private_key"],
|
||||
public_key=keypair["public_key"],
|
||||
username="test",
|
||||
domain=domain,
|
||||
name="Test User",
|
||||
|
@ -171,6 +173,7 @@ def remote_identity() -> Identity:
|
|||
domain=domain,
|
||||
name="Test Remote User",
|
||||
local=False,
|
||||
state="updated",
|
||||
)
|
||||
|
||||
|
||||
|
|
56
tests/users/models/test_follow.py
Normal file
56
tests/users/models/test_follow.py
Normal file
|
@ -0,0 +1,56 @@
|
|||
import json
|
||||
|
||||
import pytest
|
||||
from pytest_httpx import HTTPXMock
|
||||
|
||||
from users.models import Follow, FollowStates, Identity, InboxMessage
|
||||
from users.services import IdentityService
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize("ref_only", [True, False])
|
||||
def test_follow(
|
||||
identity: Identity,
|
||||
remote_identity: Identity,
|
||||
stator,
|
||||
httpx_mock: HTTPXMock,
|
||||
ref_only: bool,
|
||||
):
|
||||
"""
|
||||
Ensures that follow sending and acceptance works
|
||||
"""
|
||||
# Make the follow
|
||||
follow = IdentityService(remote_identity).follow_from(identity)
|
||||
assert Follow.objects.get(pk=follow.pk).state == FollowStates.unrequested
|
||||
# Run stator to make it try and send out the remote request
|
||||
httpx_mock.add_response(
|
||||
url="https://remote.test/@test/inbox/",
|
||||
status_code=202,
|
||||
)
|
||||
stator.run_single_cycle_sync()
|
||||
outbound_data = json.loads(httpx_mock.get_request().content)
|
||||
assert outbound_data["type"] == "Follow"
|
||||
assert outbound_data["actor"] == identity.actor_uri
|
||||
assert outbound_data["object"] == remote_identity.actor_uri
|
||||
assert outbound_data["id"] == f"{identity.actor_uri}follow/{follow.pk}/"
|
||||
assert Follow.objects.get(pk=follow.pk).state == FollowStates.local_requested
|
||||
# Come in with an inbox message of either a reference type or an embedded type
|
||||
if ref_only:
|
||||
message = {
|
||||
"type": "Accept",
|
||||
"id": "test",
|
||||
"actor": remote_identity.actor_uri,
|
||||
"object": outbound_data["id"],
|
||||
}
|
||||
else:
|
||||
del outbound_data["@context"]
|
||||
message = {
|
||||
"type": "Accept",
|
||||
"id": "test",
|
||||
"actor": remote_identity.actor_uri,
|
||||
"object": outbound_data,
|
||||
}
|
||||
InboxMessage.objects.create(message=message)
|
||||
# Run stator and ensure that accepted our follow
|
||||
stator.run_single_cycle_sync()
|
||||
assert Follow.objects.get(pk=follow.pk).state == FollowStates.accepted
|
|
@ -272,6 +272,28 @@ class Follow(StatorModel):
|
|||
]:
|
||||
follow.transition_perform(FollowStates.accepted)
|
||||
|
||||
@classmethod
|
||||
def handle_accept_ref_ap(cls, data):
|
||||
"""
|
||||
Handles an incoming Follow Accept for one of our follows where there is
|
||||
only an object URI reference.
|
||||
"""
|
||||
# Ensure the object ref is in a format we expect
|
||||
bits = data["object"].strip("/").split("/")
|
||||
if bits[-2] != "follow":
|
||||
raise ValueError(f"Unknown Follow object URI in Accept: {data['object']}")
|
||||
# Retrieve the object by PK
|
||||
follow = cls.objects.get(pk=bits[-1])
|
||||
# Ensure it's from the right actor
|
||||
if data["actor"] != follow.target.actor_uri:
|
||||
raise ValueError("Accept actor does not match its Follow object", data)
|
||||
# If the follow was waiting to be accepted, transition it
|
||||
if follow.state in [
|
||||
FollowStates.unrequested,
|
||||
FollowStates.local_requested,
|
||||
]:
|
||||
follow.transition_perform(FollowStates.accepted)
|
||||
|
||||
@classmethod
|
||||
def handle_undo_ap(cls, data):
|
||||
"""
|
||||
|
|
|
@ -65,6 +65,10 @@ class InboxMessageStates(StateGraph):
|
|||
match instance.message_object_type:
|
||||
case "follow":
|
||||
await sync_to_async(Follow.handle_accept_ap)(instance.message)
|
||||
case None:
|
||||
await sync_to_async(Follow.handle_accept_ref_ap)(
|
||||
instance.message
|
||||
)
|
||||
case unknown:
|
||||
raise ValueError(
|
||||
f"Cannot handle activity of type accept.{unknown}"
|
||||
|
@ -113,6 +117,9 @@ class InboxMessageStates(StateGraph):
|
|||
case "remove":
|
||||
# We are ignoring these right now (probably pinned items)
|
||||
pass
|
||||
case "move":
|
||||
# We're ignoring moves for now
|
||||
pass
|
||||
case "http://litepub.social/ns#emojireact":
|
||||
# We're ignoring emoji reactions for now
|
||||
pass
|
||||
|
|
|
@ -38,7 +38,7 @@ class IdentityService:
|
|||
"""
|
||||
existing_follow = Follow.maybe_get(from_identity, self.identity)
|
||||
if not existing_follow:
|
||||
Follow.create_local(from_identity, self.identity)
|
||||
return Follow.create_local(from_identity, self.identity)
|
||||
elif existing_follow.state not in FollowStates.group_active():
|
||||
existing_follow.transition_perform(FollowStates.unrequested)
|
||||
return cast(Follow, existing_follow)
|
||||
|
|
Loading…
Reference in a new issue