parent
d525a2f465
commit
716b74404f
|
@ -106,13 +106,15 @@ def domain2() -> Domain:
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@pytest.mark.django_db
|
@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.
|
Creates a basic test identity with a user and domain.
|
||||||
"""
|
"""
|
||||||
identity = Identity.objects.create(
|
identity = Identity.objects.create(
|
||||||
actor_uri="https://example.com/@test@example.com/",
|
actor_uri="https://example.com/@test@example.com/",
|
||||||
inbox_uri="https://example.com/@test@example.com/inbox/",
|
inbox_uri="https://example.com/@test@example.com/inbox/",
|
||||||
|
private_key=keypair["private_key"],
|
||||||
|
public_key=keypair["public_key"],
|
||||||
username="test",
|
username="test",
|
||||||
domain=domain,
|
domain=domain,
|
||||||
name="Test User",
|
name="Test User",
|
||||||
|
@ -171,6 +173,7 @@ def remote_identity() -> Identity:
|
||||||
domain=domain,
|
domain=domain,
|
||||||
name="Test Remote User",
|
name="Test Remote User",
|
||||||
local=False,
|
local=False,
|
||||||
|
state="updated",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
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
|
@classmethod
|
||||||
def handle_undo_ap(cls, data):
|
def handle_undo_ap(cls, data):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -65,6 +65,10 @@ class InboxMessageStates(StateGraph):
|
||||||
match instance.message_object_type:
|
match instance.message_object_type:
|
||||||
case "follow":
|
case "follow":
|
||||||
await sync_to_async(Follow.handle_accept_ap)(instance.message)
|
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:
|
case unknown:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Cannot handle activity of type accept.{unknown}"
|
f"Cannot handle activity of type accept.{unknown}"
|
||||||
|
@ -113,6 +117,9 @@ class InboxMessageStates(StateGraph):
|
||||||
case "remove":
|
case "remove":
|
||||||
# We are ignoring these right now (probably pinned items)
|
# We are ignoring these right now (probably pinned items)
|
||||||
pass
|
pass
|
||||||
|
case "move":
|
||||||
|
# We're ignoring moves for now
|
||||||
|
pass
|
||||||
case "http://litepub.social/ns#emojireact":
|
case "http://litepub.social/ns#emojireact":
|
||||||
# We're ignoring emoji reactions for now
|
# We're ignoring emoji reactions for now
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -38,7 +38,7 @@ class IdentityService:
|
||||||
"""
|
"""
|
||||||
existing_follow = Follow.maybe_get(from_identity, self.identity)
|
existing_follow = Follow.maybe_get(from_identity, self.identity)
|
||||||
if not existing_follow:
|
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():
|
elif existing_follow.state not in FollowStates.group_active():
|
||||||
existing_follow.transition_perform(FollowStates.unrequested)
|
existing_follow.transition_perform(FollowStates.unrequested)
|
||||||
return cast(Follow, existing_follow)
|
return cast(Follow, existing_follow)
|
||||||
|
|
Loading…
Reference in New Issue