Removed too complex FollowRequest model, we now use an aproved field on Follow

This commit is contained in:
Eliot Berriot 2018-04-10 21:25:35 +02:00
parent c97db31cb1
commit 0b2fe8439a
No known key found for this signature in database
GPG Key ID: DD6965E2476E5C27
14 changed files with 480 additions and 306 deletions

View File

@ -6,8 +6,10 @@ import uuid
from django.conf import settings from django.conf import settings
from funkwhale_api.common import session from funkwhale_api.common import session
from funkwhale_api.common import utils as funkwhale_utils
from . import models from . import models
from . import serializers
from . import signing from . import signing
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -85,66 +87,9 @@ def deliver(activity, on_behalf_of, to=[]):
logger.debug('Remote answered with %s', response.status_code) logger.debug('Remote answered with %s', response.status_code)
def get_follow(follow_id, follower, followed): def accept_follow(follow):
return { serializer = serializers.AcceptFollowSerializer(follow)
'@context': [
'https://www.w3.org/ns/activitystreams',
'https://w3id.org/security/v1',
{}
],
'actor': follower.url,
'id': follower.url + '#follows/{}'.format(follow_id),
'object': followed.url,
'type': 'Follow'
}
def get_undo(id, actor, object):
return {
'@context': [
'https://www.w3.org/ns/activitystreams',
'https://w3id.org/security/v1',
{}
],
'type': 'Undo',
'id': id + '/undo',
'actor': actor.url,
'object': object,
}
def get_accept_follow(accept_id, accept_actor, follow, follow_actor):
return {
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{}
],
"id": accept_actor.url + '#accepts/follows/{}'.format(
accept_id),
"type": "Accept",
"actor": accept_actor.url,
"object": {
"id": follow['id'],
"type": "Follow",
"actor": follow_actor.url,
"object": accept_actor.url
},
}
def accept_follow(target, follow, actor):
accept_uuid = uuid.uuid4()
accept = get_accept_follow(
accept_id=accept_uuid,
accept_actor=target,
follow=follow,
follow_actor=actor)
deliver( deliver(
accept, serializer.data,
to=[actor.url], to=[follow.actor.url],
on_behalf_of=target) on_behalf_of=follow.target)
return models.Follow.objects.get_or_create(
actor=actor,
target=target,
)

View File

@ -153,24 +153,32 @@ class SystemActor(object):
def handle_follow(self, ac, sender): def handle_follow(self, ac, sender):
system_actor = self.get_actor_instance() system_actor = self.get_actor_instance()
if self.manually_approves_followers: serializer = serializers.FollowSerializer(
fr, created = models.FollowRequest.objects.get_or_create( data=ac, context={'follow_actor': sender})
actor=sender, if not serializer.is_valid():
target=system_actor, return logger.info('Invalid follow payload')
approved=None, approved = True if not self.manually_approves_followers else None
) follow = serializer.save(approved=approved)
return fr if follow.approved:
return activity.accept_follow(follow)
return activity.accept_follow( def handle_accept(self, ac, sender):
system_actor, ac, sender system_actor = self.get_actor_instance()
) serializer = serializers.AcceptFollowSerializer(
data=ac,
context={'follow_target': sender, 'follow_actor': system_actor})
if not serializer.is_valid(raise_exception=True):
return logger.info('Received invalid payload')
serializer.save()
def handle_undo_follow(self, ac, sender): def handle_undo_follow(self, ac, sender):
actor = self.get_actor_instance() system_actor = self.get_actor_instance()
models.Follow.objects.filter( serializer = serializers.UndoFollowSerializer(
actor=sender, data=ac, context={'actor': sender, 'target': system_actor})
target=actor, if not serializer.is_valid():
).delete() return logger.info('Received invalid payload')
serializer.save()
def handle_undo(self, ac, sender): def handle_undo(self, ac, sender):
if ac['object']['type'] != 'Follow': if ac['object']['type'] != 'Follow':
@ -206,20 +214,6 @@ class LibraryActor(SystemActor):
def manually_approves_followers(self): def manually_approves_followers(self):
return settings.FEDERATION_MUSIC_NEEDS_APPROVAL return settings.FEDERATION_MUSIC_NEEDS_APPROVAL
def handle_follow(self, ac, sender):
system_actor = self.get_actor_instance()
if self.manually_approves_followers:
fr, created = models.FollowRequest.objects.get_or_create(
actor=sender,
target=system_actor,
approved=None,
)
return fr
return activity.accept_follow(
system_actor, ac, sender
)
@transaction.atomic @transaction.atomic
def handle_create(self, ac, sender): def handle_create(self, ac, sender):
try: try:
@ -360,15 +354,15 @@ class TestActor(SystemActor):
super().handle_follow(ac, sender) super().handle_follow(ac, sender)
# also, we follow back # also, we follow back
test_actor = self.get_actor_instance() test_actor = self.get_actor_instance()
follow_uuid = uuid.uuid4() follow_back = models.Follow.objects.get_or_create(
follow = activity.get_follow( actor=test_actor,
follow_id=follow_uuid, target=sender,
follower=test_actor, approved=None,
followed=sender) )[0]
activity.deliver( activity.deliver(
follow, serializers.FollowSerializer(follow_back).data,
to=[ac['actor']], to=[follow_back.target.url],
on_behalf_of=test_actor) on_behalf_of=follow_back.actor)
def handle_undo_follow(self, ac, sender): def handle_undo_follow(self, ac, sender):
super().handle_undo_follow(ac, sender) super().handle_undo_follow(ac, sender)
@ -381,11 +375,7 @@ class TestActor(SystemActor):
) )
except models.Follow.DoesNotExist: except models.Follow.DoesNotExist:
return return
undo = activity.get_undo( undo = serializers.UndoFollowSerializer(follow).data
id=follow.get_federation_url(),
actor=actor,
object=serializers.FollowSerializer(follow).data,
)
follow.delete() follow.delete()
activity.deliver( activity.deliver(
undo, undo,

View File

@ -23,24 +23,13 @@ class FollowAdmin(admin.ModelAdmin):
list_display = [ list_display = [
'actor', 'actor',
'target', 'target',
'approved',
'creation_date' 'creation_date'
] ]
search_fields = ['actor__url', 'target__url']
list_select_related = True
@admin.register(models.FollowRequest)
class FollowRequestAdmin(admin.ModelAdmin):
list_display = [
'actor',
'target',
'creation_date',
'approved'
]
search_fields = ['actor__url', 'target__url']
list_filter = [ list_filter = [
'approved' 'approved'
] ]
search_fields = ['actor__url', 'target__url']
list_select_related = True list_select_related = True

View File

@ -113,15 +113,6 @@ class FollowFactory(factory.DjangoModelFactory):
) )
@registry.register
class FollowRequestFactory(factory.DjangoModelFactory):
target = factory.SubFactory(ActorFactory)
actor = factory.SubFactory(ActorFactory)
class Meta:
model = models.FollowRequest
@registry.register @registry.register
class LibraryFactory(factory.DjangoModelFactory): class LibraryFactory(factory.DjangoModelFactory):
actor = factory.SubFactory(ActorFactory) actor = factory.SubFactory(ActorFactory)

View File

@ -38,25 +38,21 @@ def scan_from_account_name(account_name):
actor__domain=domain, actor__domain=domain,
actor__preferred_username=username actor__preferred_username=username
).select_related('actor').first() ).select_related('actor').first()
follow_request = None data['local'] = {
if library: 'following': False,
data['local']['following'] = True 'awaiting_approval': False,
data['local']['awaiting_approval'] = True }
try:
else: follow = models.Follow.objects.get(
follow_request = models.FollowRequest.objects.filter(
target__preferred_username=username, target__preferred_username=username,
target__domain=username, target__domain=username,
actor=system_library, actor=system_library,
).first() )
data['local'] = { data['local']['awaiting_approval'] = not bool(follow.approved)
'following': False, data['local']['following'] = True
'awaiting_approval': False, except models.Follow.DoesNotExist:
} pass
if follow_request:
data['awaiting_approval'] = follow_request.approved is None
follow_request = models.Follow
try: try:
data['webfinger'] = webfinger.get_resource( data['webfinger'] = webfinger.get_resource(
'acct:{}'.format(account_name)) 'acct:{}'.format(account_name))

View File

@ -0,0 +1,29 @@
# Generated by Django 2.0.3 on 2018-04-10 16:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('federation', '0003_auto_20180407_1010'),
]
operations = [
migrations.RemoveField(
model_name='followrequest',
name='actor',
),
migrations.RemoveField(
model_name='followrequest',
name='target',
),
migrations.AddField(
model_name='follow',
name='approved',
field=models.NullBooleanField(default=None),
),
migrations.DeleteModel(
name='FollowRequest',
),
]

View File

@ -109,6 +109,7 @@ class Follow(models.Model):
creation_date = models.DateTimeField(default=timezone.now) creation_date = models.DateTimeField(default=timezone.now)
modification_date = models.DateTimeField( modification_date = models.DateTimeField(
auto_now=True) auto_now=True)
approved = models.NullBooleanField(default=None)
class Meta: class Meta:
unique_together = ['actor', 'target'] unique_together = ['actor', 'target']
@ -117,49 +118,6 @@ class Follow(models.Model):
return '{}#follows/{}'.format(self.actor.url, self.uuid) return '{}#follows/{}'.format(self.actor.url, self.uuid)
class FollowRequest(models.Model):
uuid = models.UUIDField(default=uuid.uuid4, unique=True)
actor = models.ForeignKey(
Actor,
related_name='emmited_follow_requests',
on_delete=models.CASCADE,
)
target = models.ForeignKey(
Actor,
related_name='received_follow_requests',
on_delete=models.CASCADE,
)
creation_date = models.DateTimeField(default=timezone.now)
modification_date = models.DateTimeField(
auto_now=True)
approved = models.NullBooleanField(default=None)
def approve(self):
from . import activity
from . import serializers
self.approved = True
self.save(update_fields=['approved'])
Follow.objects.get_or_create(
target=self.target,
actor=self.actor
)
if self.target.is_local:
follow = {
'@context': serializers.AP_CONTEXT,
'actor': self.actor.url,
'id': self.actor.url + '#follows/{}'.format(uuid.uuid4()),
'object': self.target.url,
'type': 'Follow'
}
activity.accept_follow(
self.target, follow, self.actor
)
def refuse(self):
self.approved = False
self.save(update_fields=['approved'])
class Library(models.Model): class Library(models.Model):
creation_date = models.DateTimeField(default=timezone.now) creation_date = models.DateTimeField(default=timezone.now)
modification_date = models.DateTimeField( modification_date = models.DateTimeField(

View File

@ -121,28 +121,132 @@ class LibraryActorSerializer(ActorSerializer):
return validated_data return validated_data
class FollowSerializer(serializers.ModelSerializer): class FollowSerializer(serializers.Serializer):
# left maps to activitypub fields, right to our internal models id = serializers.URLField()
id = serializers.URLField(source='get_federation_url') object = serializers.URLField()
object = serializers.URLField(source='target.url') actor = serializers.URLField()
actor = serializers.URLField(source='actor.url') type = serializers.ChoiceField(choices=['Follow'])
type = serializers.CharField(source='ap_type')
class Meta: def validate_object(self, v):
model = models.Actor expected = self.context.get('follow_target')
fields = [ if expected and expected.url != v:
'id', raise serializers.ValidationError('Invalid target')
'object', try:
'actor', return models.Actor.objects.get(url=v)
'type' except models.Actor.DoesNotExist:
] raise serializers.ValidationError('Target not found')
def validate_actor(self, v):
expected = self.context.get('follow_actor')
if expected and expected.url != v:
raise serializers.ValidationError('Invalid actor')
try:
return models.Actor.objects.get(url=v)
except models.Actor.DoesNotExist:
raise serializers.ValidationError('Actor not found')
def save(self, **kwargs):
return models.Follow.objects.get_or_create(
actor=self.validated_data['actor'],
target=self.validated_data['object'],
**kwargs,
)[0]
def to_representation(self, instance): def to_representation(self, instance):
ret = super().to_representation(instance) return {
ret['@context'] = AP_CONTEXT '@context': AP_CONTEXT,
'actor': instance.actor.url,
'id': instance.get_federation_url(),
'object': instance.target.url,
'type': 'Follow'
}
return ret return ret
class AcceptFollowSerializer(serializers.Serializer):
id = serializers.URLField()
actor = serializers.URLField()
object = FollowSerializer()
type = serializers.ChoiceField(choices=['Accept'])
def validate_actor(self, v):
expected = self.context.get('follow_target')
if expected and expected.url != v:
raise serializers.ValidationError('Invalid actor')
try:
return models.Actor.objects.get(url=v)
except models.Actor.DoesNotExist:
raise serializers.ValidationError('Actor not found')
def validate(self, validated_data):
# we ensure the accept actor actually match the follow target
if validated_data['actor'] != validated_data['object']['object']:
raise serializers.ValidationError('Actor mismatch')
try:
validated_data['follow'] = models.Follow.objects.filter(
target=validated_data['actor'],
actor=validated_data['object']['actor']
).exclude(approved=True).get()
except models.Follow.DoesNotExist:
raise serializers.ValidationError('No follow to accept')
return validated_data
def to_representation(self, instance):
return {
"@context": AP_CONTEXT,
"id": instance.get_federation_url() + '/accept',
"type": "Accept",
"actor": instance.target.url,
"object": FollowSerializer(instance).data
}
def save(self):
self.validated_data['follow'].approved = True
self.validated_data['follow'].save()
return self.validated_data['follow']
class UndoFollowSerializer(serializers.Serializer):
id = serializers.URLField()
actor = serializers.URLField()
object = FollowSerializer()
type = serializers.ChoiceField(choices=['Undo'])
def validate_actor(self, v):
expected = self.context.get('follow_target')
if expected and expected.url != v:
raise serializers.ValidationError('Invalid actor')
try:
return models.Actor.objects.get(url=v)
except models.Actor.DoesNotExist:
raise serializers.ValidationError('Actor not found')
def validate(self, validated_data):
# we ensure the accept actor actually match the follow actor
if validated_data['actor'] != validated_data['object']['actor']:
raise serializers.ValidationError('Actor mismatch')
try:
validated_data['follow'] = models.Follow.objects.filter(
actor=validated_data['actor'],
target=validated_data['object']['object']
).get()
except models.Follow.DoesNotExist:
raise serializers.ValidationError('No follow to remove')
return validated_data
def to_representation(self, instance):
return {
"@context": AP_CONTEXT,
"id": instance.get_federation_url() + '/undo',
"type": "Undo",
"actor": instance.actor.url,
"object": FollowSerializer(instance).data
}
def save(self):
self.validated_data['follow'].delete()
class ActorWebfingerSerializer(serializers.Serializer): class ActorWebfingerSerializer(serializers.Serializer):
subject = serializers.CharField() subject = serializers.CharField()
aliases = serializers.ListField(child=serializers.URLField()) aliases = serializers.ListField(child=serializers.URLField())

View File

@ -1,6 +1,7 @@
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.core import paginator from django.core import paginator
from django.db import transaction
from django.http import HttpResponse from django.http import HttpResponse
from django.urls import reverse from django.urls import reverse
@ -9,9 +10,12 @@ from rest_framework import response
from rest_framework import views from rest_framework import views
from rest_framework import viewsets from rest_framework import viewsets
from rest_framework.decorators import list_route, detail_route from rest_framework.decorators import list_route, detail_route
from rest_framework.serializers import ValidationError
from funkwhale_api.common import utils as funkwhale_utils
from funkwhale_api.music.models import TrackFile from funkwhale_api.music.models import TrackFile
from . import activity
from . import actors from . import actors
from . import authentication from . import authentication
from . import library from . import library
@ -172,3 +176,29 @@ class LibraryViewSet(viewsets.GenericViewSet):
data = library.scan_from_account_name(account) data = library.scan_from_account_name(account)
return response.Response(data) return response.Response(data)
@transaction.atomic
def create(self, request, *args, **kwargs):
try:
actor_url = request.data['actor_url']
except KeyError:
raise ValidationError('Missing actor_url')
try:
actor = actors.get_actor(actor_url)
library_data = library.get_library_data(actor.url)
except Exception as e:
raise ValidationError('Error while fetching actor and library')
library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
follow, created = models.Follow.objects.get_or_create(
actor=library_actor,
target=actor,
)
serializer = serializers.FollowSerializer(follow)
activity.deliver(
serializer.data,
on_behalf_of=library_actor,
to=[actor.url]
)
return response.Response({}, status=201)

View File

@ -1,6 +1,7 @@
import uuid import uuid
from funkwhale_api.federation import activity from funkwhale_api.federation import activity
from funkwhale_api.federation import serializers
def test_deliver(nodb_factories, r_mock, mocker): def test_deliver(nodb_factories, r_mock, mocker):
@ -38,37 +39,9 @@ def test_deliver(nodb_factories, r_mock, mocker):
def test_accept_follow(mocker, factories): def test_accept_follow(mocker, factories):
deliver = mocker.patch( deliver = mocker.patch(
'funkwhale_api.federation.activity.deliver') 'funkwhale_api.federation.activity.deliver')
actor = factories['federation.Actor']() follow = factories['federation.Follow'](approved=None)
target = factories['federation.Actor'](local=True) expected_accept = serializers.AcceptFollowSerializer(follow).data
follow = { activity.accept_follow(follow)
'actor': actor.url,
'type': 'Follow',
'id': 'http://test.federation/user#follows/267',
'object': target.url,
}
uid = uuid.uuid4()
mocker.patch('uuid.uuid4', return_value=uid)
expected_accept = {
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{}
],
"id": target.url + '#accepts/follows/{}'.format(uid),
"type": "Accept",
"actor": target.url,
"object": {
"id": follow['id'],
"type": "Follow",
"actor": actor.url,
"object": target.url
},
}
activity.accept_follow(
target, follow, actor
)
deliver.assert_called_once_with( deliver.assert_called_once_with(
expected_accept, to=[actor.url], on_behalf_of=target expected_accept, to=[follow.actor.url], on_behalf_of=follow.target
) )
follow_instance = actor.emitted_follows.first()
assert follow_instance.target == target

View File

@ -7,6 +7,7 @@ from django.utils import timezone
from rest_framework import exceptions from rest_framework import exceptions
from funkwhale_api.federation import activity
from funkwhale_api.federation import actors from funkwhale_api.federation import actors
from funkwhale_api.federation import models from funkwhale_api.federation import models
from funkwhale_api.federation import serializers from funkwhale_api.federation import serializers
@ -261,8 +262,6 @@ def test_test_actor_handles_follow(
deliver = mocker.patch( deliver = mocker.patch(
'funkwhale_api.federation.activity.deliver') 'funkwhale_api.federation.activity.deliver')
actor = factories['federation.Actor']() actor = factories['federation.Actor']()
now = timezone.now()
mocker.patch('django.utils.timezone.now', return_value=now)
accept_follow = mocker.patch( accept_follow = mocker.patch(
'funkwhale_api.federation.activity.accept_follow') 'funkwhale_api.federation.activity.accept_follow')
test_actor = actors.SYSTEM_ACTORS['test'].get_actor_instance() test_actor = actors.SYSTEM_ACTORS['test'].get_actor_instance()
@ -272,28 +271,15 @@ def test_test_actor_handles_follow(
'id': 'http://test.federation/user#follows/267', 'id': 'http://test.federation/user#follows/267',
'object': test_actor.url, 'object': test_actor.url,
} }
uid = uuid.uuid4()
mocker.patch('uuid.uuid4', return_value=uid)
expected_follow = {
'@context': serializers.AP_CONTEXT,
'actor': test_actor.url,
'id': test_actor.url + '#follows/{}'.format(uid),
'object': actor.url,
'type': 'Follow'
}
actors.SYSTEM_ACTORS['test'].post_inbox(data, actor=actor) actors.SYSTEM_ACTORS['test'].post_inbox(data, actor=actor)
accept_follow.assert_called_once_with( follow = models.Follow.objects.get(target=test_actor, approved=True)
test_actor, data, actor follow_back = models.Follow.objects.get(actor=test_actor, approved=None)
accept_follow.assert_called_once_with(follow)
deliver.assert_called_once_with(
serializers.FollowSerializer(follow_back).data,
on_behalf_of=test_actor,
to=[actor.url]
) )
expected_calls = [
mocker.call(
expected_follow,
to=[actor.url],
on_behalf_of=test_actor,
)
]
deliver.assert_has_calls(expected_calls)
def test_test_actor_handles_undo_follow( def test_test_actor_handles_undo_follow(
@ -346,12 +332,10 @@ def test_library_actor_handles_follow_manual_approval(
} }
library_actor.system_conf.post_inbox(data, actor=actor) library_actor.system_conf.post_inbox(data, actor=actor)
fr = library_actor.received_follow_requests.first() follow = library_actor.received_follows.first()
assert library_actor.received_follow_requests.count() == 1 assert follow.actor == actor
assert fr.target == library_actor assert follow.approved is None
assert fr.actor == actor
assert fr.approved is None
def test_library_actor_handles_follow_auto_approval( def test_library_actor_handles_follow_auto_approval(
@ -369,10 +353,27 @@ def test_library_actor_handles_follow_auto_approval(
} }
library_actor.system_conf.post_inbox(data, actor=actor) library_actor.system_conf.post_inbox(data, actor=actor)
assert library_actor.received_follow_requests.count() == 0 follow = library_actor.received_follows.first()
accept_follow.assert_called_once_with(
library_actor, data, actor assert follow.actor == actor
assert follow.approved is True
def test_library_actor_handles_accept(
mocker, factories):
library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
actor = factories['federation.Actor']()
pending_follow = factories['federation.Follow'](
actor=library_actor,
target=actor,
approved=None,
) )
serializer = serializers.AcceptFollowSerializer(pending_follow)
library_actor.system_conf.post_inbox(serializer.data, actor=actor)
pending_follow.refresh_from_db()
assert pending_follow.approved is True
def test_library_actor_handle_create_audio_no_library(mocker, factories): def test_library_actor_handle_create_audio_no_library(mocker, factories):

View File

@ -35,50 +35,6 @@ def test_follow_federation_url(factories):
assert follow.get_federation_url() == expected assert follow.get_federation_url() == expected
def test_follow_request_approve(mocker, factories):
uid = uuid.uuid4()
mocker.patch('uuid.uuid4', return_value=uid)
accept_follow = mocker.patch(
'funkwhale_api.federation.activity.accept_follow')
fr = factories['federation.FollowRequest'](target__local=True)
fr.approve()
follow = {
'@context': serializers.AP_CONTEXT,
'actor': fr.actor.url,
'id': fr.actor.url + '#follows/{}'.format(uid),
'object': fr.target.url,
'type': 'Follow'
}
assert fr.approved is True
assert list(fr.target.followers.all()) == [fr.actor]
accept_follow.assert_called_once_with(
fr.target, follow, fr.actor
)
def test_follow_request_approve_non_local(mocker, factories):
uid = uuid.uuid4()
mocker.patch('uuid.uuid4', return_value=uid)
accept_follow = mocker.patch(
'funkwhale_api.federation.activity.accept_follow')
fr = factories['federation.FollowRequest']()
fr.approve()
assert fr.approved is True
assert list(fr.target.followers.all()) == [fr.actor]
accept_follow.assert_not_called()
def test_follow_request_refused(mocker, factories):
fr = factories['federation.FollowRequest']()
fr.refuse()
assert fr.approved is False
assert fr.target.followers.count() == 0
def test_library_model_unique_per_actor(factories): def test_library_model_unique_per_actor(factories):
library = factories['federation.Library']() library = factories['federation.Library']()
with pytest.raises(db.IntegrityError): with pytest.raises(db.IntegrityError):

View File

@ -1,4 +1,5 @@
import arrow import arrow
import pytest
from django.urls import reverse from django.urls import reverse
from django.core.paginator import Paginator from django.core.paginator import Paginator
@ -170,6 +171,184 @@ def test_follow_serializer_to_ap(factories):
assert serializer.data == expected assert serializer.data == expected
def test_follow_serializer_save(factories):
actor = factories['federation.Actor']()
target = factories['federation.Actor']()
data = expected = {
'id': 'https://test.follow',
'type': 'Follow',
'actor': actor.url,
'object': target.url,
}
serializer = serializers.FollowSerializer(data=data)
assert serializer.is_valid(raise_exception=True)
follow = serializer.save()
assert follow.pk is not None
assert follow.actor == actor
assert follow.target == target
assert follow.approved is None
def test_follow_serializer_save_validates_on_context(factories):
actor = factories['federation.Actor']()
target = factories['federation.Actor']()
impostor = factories['federation.Actor']()
data = expected = {
'id': 'https://test.follow',
'type': 'Follow',
'actor': actor.url,
'object': target.url,
}
serializer = serializers.FollowSerializer(
data=data,
context={'follow_actor': impostor, 'follow_target': impostor})
assert serializer.is_valid() is False
assert 'actor' in serializer.errors
assert 'object' in serializer.errors
def test_accept_follow_serializer_representation(factories):
follow = factories['federation.Follow'](approved=None)
expected = {
'@context': [
'https://www.w3.org/ns/activitystreams',
'https://w3id.org/security/v1',
{},
],
'id': follow.get_federation_url() + '/accept',
'type': 'Accept',
'actor': follow.target.url,
'object': serializers.FollowSerializer(follow).data,
}
serializer = serializers.AcceptFollowSerializer(follow)
assert serializer.data == expected
def test_accept_follow_serializer_save(factories):
follow = factories['federation.Follow'](approved=None)
data = {
'@context': [
'https://www.w3.org/ns/activitystreams',
'https://w3id.org/security/v1',
{},
],
'id': follow.get_federation_url() + '/accept',
'type': 'Accept',
'actor': follow.target.url,
'object': serializers.FollowSerializer(follow).data,
}
serializer = serializers.AcceptFollowSerializer(data=data)
assert serializer.is_valid(raise_exception=True)
serializer.save()
follow.refresh_from_db()
assert follow.approved is True
def test_accept_follow_serializer_validates_on_context(factories):
follow = factories['federation.Follow'](approved=None)
impostor = factories['federation.Actor']()
data = {
'@context': [
'https://www.w3.org/ns/activitystreams',
'https://w3id.org/security/v1',
{},
],
'id': follow.get_federation_url() + '/accept',
'type': 'Accept',
'actor': impostor.url,
'object': serializers.FollowSerializer(follow).data,
}
serializer = serializers.AcceptFollowSerializer(
data=data,
context={'follow_actor': impostor, 'follow_target': impostor})
assert serializer.is_valid() is False
assert 'actor' in serializer.errors['object']
assert 'object' in serializer.errors['object']
def test_undo_follow_serializer_representation(factories):
follow = factories['federation.Follow'](approved=True)
expected = {
'@context': [
'https://www.w3.org/ns/activitystreams',
'https://w3id.org/security/v1',
{},
],
'id': follow.get_federation_url() + '/undo',
'type': 'Undo',
'actor': follow.actor.url,
'object': serializers.FollowSerializer(follow).data,
}
serializer = serializers.UndoFollowSerializer(follow)
assert serializer.data == expected
def test_undo_follow_serializer_save(factories):
follow = factories['federation.Follow'](approved=True)
data = {
'@context': [
'https://www.w3.org/ns/activitystreams',
'https://w3id.org/security/v1',
{},
],
'id': follow.get_federation_url() + '/undo',
'type': 'Undo',
'actor': follow.actor.url,
'object': serializers.FollowSerializer(follow).data,
}
serializer = serializers.UndoFollowSerializer(data=data)
assert serializer.is_valid(raise_exception=True)
serializer.save()
with pytest.raises(models.Follow.DoesNotExist):
follow.refresh_from_db()
def test_undo_follow_serializer_validates_on_context(factories):
follow = factories['federation.Follow'](approved=True)
impostor = factories['federation.Actor']()
data = {
'@context': [
'https://www.w3.org/ns/activitystreams',
'https://w3id.org/security/v1',
{},
],
'id': follow.get_federation_url() + '/undo',
'type': 'Undo',
'actor': impostor.url,
'object': serializers.FollowSerializer(follow).data,
}
serializer = serializers.UndoFollowSerializer(
data=data,
context={'follow_actor': impostor, 'follow_target': impostor})
assert serializer.is_valid() is False
assert 'actor' in serializer.errors['object']
assert 'object' in serializer.errors['object']
def test_paginated_collection_serializer(factories): def test_paginated_collection_serializer(factories):
tfs = factories['music.TrackFile'].create_batch(size=5) tfs = factories['music.TrackFile'].create_batch(size=5)
actor = factories['federation.Actor'](local=True) actor = factories['federation.Actor'](local=True)

View File

@ -4,6 +4,7 @@ from django.core.paginator import Paginator
import pytest import pytest
from funkwhale_api.federation import actors from funkwhale_api.federation import actors
from funkwhale_api.federation import models
from funkwhale_api.federation import serializers from funkwhale_api.federation import serializers
from funkwhale_api.federation import utils from funkwhale_api.federation import utils
from funkwhale_api.federation import webfinger from funkwhale_api.federation import webfinger
@ -179,3 +180,35 @@ def test_can_scan_library(superuser_api_client, mocker):
assert response.status_code == 200 assert response.status_code == 200
assert response.data == result assert response.data == result
scan.assert_called_once_with('test@test.library') scan.assert_called_once_with('test@test.library')
def test_follow_library_manually(superuser_api_client, mocker, factories):
library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
actor = factories['federation.Actor'](manually_approves_followers=True)
follow = {'test': 'follow'}
deliver = mocker.patch(
'funkwhale_api.federation.activity.deliver')
actor_get = mocker.patch(
'funkwhale_api.federation.actors.get_actor',
return_value=actor)
library_get = mocker.patch(
'funkwhale_api.federation.library.get_library_data',
return_value={})
url = reverse('api:v1:federation:libraries-list')
response = superuser_api_client.post(
url, {'actor_url': actor.url})
assert response.status_code == 201
follow = models.Follow.objects.get(
actor=library_actor,
target=actor,
approved=None,
)
deliver.assert_called_once_with(
serializers.FollowSerializer(follow).data,
on_behalf_of=library_actor,
to=[actor.url]
)