See #852: improved routing logic for federation messages (support multiple objects types for one route)
This commit is contained in:
parent
1aa3f3f340
commit
9f3182caf7
19 changed files with 561 additions and 54 deletions
|
|
@ -1,6 +1,13 @@
|
|||
import pytest
|
||||
|
||||
from funkwhale_api.federation import actors, contexts, jsonld, routes, serializers
|
||||
from funkwhale_api.federation import (
|
||||
activity,
|
||||
actors,
|
||||
contexts,
|
||||
jsonld,
|
||||
routes,
|
||||
serializers,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
|
@ -8,23 +15,29 @@ from funkwhale_api.federation import actors, contexts, jsonld, routes, serialize
|
|||
[
|
||||
({"type": "Follow"}, routes.inbox_follow),
|
||||
({"type": "Accept"}, routes.inbox_accept),
|
||||
({"type": "Create", "object.type": "Audio"}, routes.inbox_create_audio),
|
||||
({"type": "Update", "object.type": "Library"}, routes.inbox_update_library),
|
||||
({"type": "Delete", "object.type": "Library"}, routes.inbox_delete_library),
|
||||
({"type": "Delete", "object.type": "Audio"}, routes.inbox_delete_audio),
|
||||
({"type": "Undo", "object.type": "Follow"}, routes.inbox_undo_follow),
|
||||
({"type": "Update", "object.type": "Artist"}, routes.inbox_update_artist),
|
||||
({"type": "Update", "object.type": "Album"}, routes.inbox_update_album),
|
||||
({"type": "Update", "object.type": "Track"}, routes.inbox_update_track),
|
||||
({"type": "Create", "object": {"type": "Audio"}}, routes.inbox_create_audio),
|
||||
(
|
||||
{"type": "Update", "object": {"type": "Library"}},
|
||||
routes.inbox_update_library,
|
||||
),
|
||||
(
|
||||
{"type": "Delete", "object": {"type": "Library"}},
|
||||
routes.inbox_delete_library,
|
||||
),
|
||||
({"type": "Delete", "object": {"type": "Audio"}}, routes.inbox_delete_audio),
|
||||
({"type": "Undo", "object": {"type": "Follow"}}, routes.inbox_undo_follow),
|
||||
({"type": "Update", "object": {"type": "Artist"}}, routes.inbox_update_artist),
|
||||
({"type": "Update", "object": {"type": "Album"}}, routes.inbox_update_album),
|
||||
({"type": "Update", "object": {"type": "Track"}}, routes.inbox_update_track),
|
||||
({"type": "Delete", "object": {"type": "Person"}}, routes.inbox_delete_actor),
|
||||
],
|
||||
)
|
||||
def test_inbox_routes(route, handler):
|
||||
for r, h in routes.inbox.routes:
|
||||
if r == route:
|
||||
assert h == handler
|
||||
return
|
||||
|
||||
assert False, "Inbox route {} not found".format(route)
|
||||
matching = [
|
||||
handler for r, handler in routes.inbox.routes if activity.match_route(r, route)
|
||||
]
|
||||
assert len(matching) == 1, "Inbox route {} not found".format(route)
|
||||
assert matching[0] == handler
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
|
@ -32,21 +45,41 @@ def test_inbox_routes(route, handler):
|
|||
[
|
||||
({"type": "Accept"}, routes.outbox_accept),
|
||||
({"type": "Follow"}, routes.outbox_follow),
|
||||
({"type": "Create", "object.type": "Audio"}, routes.outbox_create_audio),
|
||||
({"type": "Update", "object.type": "Library"}, routes.outbox_update_library),
|
||||
({"type": "Delete", "object.type": "Library"}, routes.outbox_delete_library),
|
||||
({"type": "Delete", "object.type": "Audio"}, routes.outbox_delete_audio),
|
||||
({"type": "Undo", "object.type": "Follow"}, routes.outbox_undo_follow),
|
||||
({"type": "Update", "object.type": "Track"}, routes.outbox_update_track),
|
||||
({"type": "Create", "object": {"type": "Audio"}}, routes.outbox_create_audio),
|
||||
(
|
||||
{"type": "Update", "object": {"type": "Library"}},
|
||||
routes.outbox_update_library,
|
||||
),
|
||||
(
|
||||
{"type": "Delete", "object": {"type": "Library"}},
|
||||
routes.outbox_delete_library,
|
||||
),
|
||||
({"type": "Delete", "object": {"type": "Audio"}}, routes.outbox_delete_audio),
|
||||
({"type": "Undo", "object": {"type": "Follow"}}, routes.outbox_undo_follow),
|
||||
({"type": "Update", "object": {"type": "Track"}}, routes.outbox_update_track),
|
||||
(
|
||||
{"type": "Delete", "object": {"type": "Tombstone"}},
|
||||
routes.outbox_delete_actor,
|
||||
),
|
||||
({"type": "Delete", "object": {"type": "Person"}}, routes.outbox_delete_actor),
|
||||
({"type": "Delete", "object": {"type": "Service"}}, routes.outbox_delete_actor),
|
||||
(
|
||||
{"type": "Delete", "object": {"type": "Application"}},
|
||||
routes.outbox_delete_actor,
|
||||
),
|
||||
({"type": "Delete", "object": {"type": "Group"}}, routes.outbox_delete_actor),
|
||||
(
|
||||
{"type": "Delete", "object": {"type": "Organization"}},
|
||||
routes.outbox_delete_actor,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_outbox_routes(route, handler):
|
||||
for r, h in routes.outbox.routes:
|
||||
if r == route:
|
||||
assert h == handler
|
||||
return
|
||||
|
||||
assert False, "Outbox route {} not found".format(route)
|
||||
matching = [
|
||||
handler for r, handler in routes.outbox.routes if activity.match_route(r, route)
|
||||
]
|
||||
assert len(matching) == 1, "Outbox route {} not found".format(route)
|
||||
assert matching[0] == handler
|
||||
|
||||
|
||||
def test_inbox_follow_library_autoapprove(factories, mocker):
|
||||
|
|
@ -559,3 +592,60 @@ def test_outbox_update_track(factories):
|
|||
|
||||
assert dict(activity["payload"]) == dict(expected)
|
||||
assert activity["actor"] == actors.get_service_actor()
|
||||
|
||||
|
||||
def test_outbox_delete_actor(factories):
|
||||
user = factories["users.User"]()
|
||||
actor = user.create_actor()
|
||||
|
||||
activity = list(routes.outbox_delete_actor({"actor": actor}))[0]
|
||||
expected = serializers.ActivitySerializer(
|
||||
{"type": "Delete", "object": {"id": actor.fid, "type": actor.type}}
|
||||
).data
|
||||
|
||||
expected["to"] = [contexts.AS.Public, {"type": "instances_with_followers"}]
|
||||
|
||||
assert dict(activity["payload"]) == dict(expected)
|
||||
assert activity["actor"] == actor
|
||||
|
||||
|
||||
def test_inbox_delete_actor(factories):
|
||||
remote_actor = factories["federation.Actor"]()
|
||||
serializer = serializers.ActivitySerializer(
|
||||
{
|
||||
"type": "Delete",
|
||||
"object": {"type": remote_actor.type, "id": remote_actor.fid},
|
||||
}
|
||||
)
|
||||
routes.inbox_delete_actor(
|
||||
serializer.data, context={"actor": remote_actor, "raise_exception": True}
|
||||
)
|
||||
with pytest.raises(remote_actor.__class__.DoesNotExist):
|
||||
remote_actor.refresh_from_db()
|
||||
|
||||
|
||||
def test_inbox_delete_actor_only_works_on_self(factories):
|
||||
remote_actor1 = factories["federation.Actor"]()
|
||||
remote_actor2 = factories["federation.Actor"]()
|
||||
serializer = serializers.ActivitySerializer(
|
||||
{
|
||||
"type": "Delete",
|
||||
"object": {"type": remote_actor2.type, "id": remote_actor2.fid},
|
||||
}
|
||||
)
|
||||
routes.inbox_delete_actor(
|
||||
serializer.data, context={"actor": remote_actor1, "raise_exception": True}
|
||||
)
|
||||
remote_actor2.refresh_from_db()
|
||||
|
||||
|
||||
def test_inbox_delete_actor_doesnt_delete_local_actor(factories):
|
||||
local_actor = factories["users.User"]().create_actor()
|
||||
serializer = serializers.ActivitySerializer(
|
||||
{"type": "Delete", "object": {"type": local_actor.type, "id": local_actor.fid}}
|
||||
)
|
||||
routes.inbox_delete_actor(
|
||||
serializer.data, context={"actor": local_actor, "raise_exception": True}
|
||||
)
|
||||
# actor should still be here!
|
||||
local_actor.refresh_from_db()
|
||||
|
|
|
|||
|
|
@ -220,13 +220,3 @@ def test_user_get_quota_status(factories, preferences, mocker):
|
|||
"errored": 3,
|
||||
"finished": 4,
|
||||
}
|
||||
|
||||
|
||||
def test_deleting_users_deletes_associated_actor(factories):
|
||||
actor = factories["federation.Actor"]()
|
||||
user = factories["users.User"](actor=actor)
|
||||
|
||||
user.delete()
|
||||
|
||||
with pytest.raises(actor.DoesNotExist):
|
||||
actor.refresh_from_db()
|
||||
|
|
|
|||
33
api/tests/users/test_mutations.py
Normal file
33
api/tests/users/test_mutations.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
from funkwhale_api.users import tasks
|
||||
|
||||
|
||||
def test_delete_account_mutation(mocker, factories, now):
|
||||
user = factories["users.User"](subsonic_api_token="test", password="test")
|
||||
actor = user.create_actor()
|
||||
on_commit = mocker.patch("funkwhale_api.common.utils.on_commit")
|
||||
|
||||
secret_key = user.secret_key
|
||||
set_unusable_password = mocker.spy(user, "set_unusable_password")
|
||||
factories["users.Grant"](user=user)
|
||||
factories["users.AccessToken"](user=user)
|
||||
factories["users.RefreshToken"](user=user)
|
||||
mutation = factories["common.Mutation"](
|
||||
type="delete_account", target=actor, payload={}
|
||||
)
|
||||
|
||||
mutation.apply()
|
||||
user.refresh_from_db()
|
||||
|
||||
set_unusable_password.assert_called_once_with()
|
||||
assert user.has_usable_password() is False
|
||||
assert user.subsonic_api_token is None
|
||||
assert user.secret_key is not None and user.secret_key != secret_key
|
||||
assert user.users_grant.count() == 0
|
||||
assert user.users_refreshtoken.count() == 0
|
||||
assert user.users_accesstoken.count() == 0
|
||||
on_commit.assert_called_once_with(tasks.delete_account.delay, user_id=user.pk)
|
||||
|
||||
assert mutation.previous_state == {
|
||||
"actor": {"preferred_username": actor.preferred_username},
|
||||
"user": {"username": user.username, "id": user.pk},
|
||||
}
|
||||
32
api/tests/users/test_tasks.py
Normal file
32
api/tests/users/test_tasks.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import pytest
|
||||
|
||||
from funkwhale_api.federation import routes
|
||||
from funkwhale_api.users import tasks
|
||||
|
||||
|
||||
def test_delete_account(factories, mocker):
|
||||
user = factories["users.User"]()
|
||||
actor = user.create_actor()
|
||||
library = factories["music.Library"](actor=actor)
|
||||
unrelated_library = factories["music.Library"]()
|
||||
dispatch = mocker.patch.object(routes.outbox, "dispatch")
|
||||
|
||||
tasks.delete_account(user_id=user.pk)
|
||||
|
||||
dispatch.assert_called_once_with(
|
||||
{"type": "Delete", "object": {"type": actor.type}}, context={"actor": actor}
|
||||
)
|
||||
|
||||
with pytest.raises(user.DoesNotExist):
|
||||
user.refresh_from_db()
|
||||
|
||||
with pytest.raises(library.DoesNotExist):
|
||||
library.refresh_from_db()
|
||||
|
||||
# this one shouldn't be deleted
|
||||
unrelated_library.refresh_from_db()
|
||||
actor.refresh_from_db()
|
||||
|
||||
assert actor.type == "Tombstone"
|
||||
assert actor.name is None
|
||||
assert actor.summary is None
|
||||
|
|
@ -39,7 +39,7 @@ def test_username_only_accepts_letters_and_underscores(
|
|||
def test_can_restrict_usernames(settings, preferences, db, api_client):
|
||||
url = reverse("rest_register")
|
||||
preferences["users__registration_enabled"] = True
|
||||
settings.USERNAME_BLACKLIST = ["funkwhale"]
|
||||
settings.ACCOUNT_USERNAME_BLACKLIST = ["funkwhale"]
|
||||
data = {
|
||||
"username": "funkwhale",
|
||||
"email": "contact@funkwhale.io",
|
||||
|
|
@ -333,3 +333,57 @@ def test_creating_user_sends_confirmation_email(
|
|||
confirmation_message = mailoutbox[-1]
|
||||
assert "Hello world" in confirmation_message.body
|
||||
assert settings.FUNKWHALE_HOSTNAME in confirmation_message.body
|
||||
|
||||
|
||||
def test_user_account_deletion_requires_valid_password(logged_in_api_client):
|
||||
user = logged_in_api_client.user
|
||||
user.set_password("mypassword")
|
||||
url = reverse("api:v1:users:users-me")
|
||||
payload = {"password": "invalid", "confirm": True}
|
||||
response = logged_in_api_client.delete(url, payload)
|
||||
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
def test_user_account_deletion_requires_confirmation(logged_in_api_client):
|
||||
user = logged_in_api_client.user
|
||||
user.set_password("mypassword")
|
||||
url = reverse("api:v1:users:users-me")
|
||||
payload = {"password": "mypassword", "confirm": False}
|
||||
response = logged_in_api_client.delete(url, payload)
|
||||
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
def test_user_account_deletion_triggers_delete_account(logged_in_api_client, mocker):
|
||||
user = logged_in_api_client.user
|
||||
user.set_password("mypassword")
|
||||
url = reverse("api:v1:users:users-me")
|
||||
payload = {"password": "mypassword", "confirm": True}
|
||||
delete_account = mocker.patch("funkwhale_api.users.tasks.delete_account.delay")
|
||||
response = logged_in_api_client.delete(url, payload)
|
||||
|
||||
assert response.status_code == 204
|
||||
delete_account.assert_called_once_with(user_id=user.pk)
|
||||
|
||||
|
||||
def test_username_with_existing_local_account_are_invalid(
|
||||
settings, preferences, factories, api_client
|
||||
):
|
||||
actor = factories["users.User"]().create_actor()
|
||||
user = actor.user
|
||||
user.delete()
|
||||
url = reverse("rest_register")
|
||||
preferences["users__registration_enabled"] = True
|
||||
settings.ACCOUNT_USERNAME_BLACKLIST = []
|
||||
data = {
|
||||
"username": user.username,
|
||||
"email": "contact@funkwhale.io",
|
||||
"password1": "testtest",
|
||||
"password2": "testtest",
|
||||
}
|
||||
|
||||
response = api_client.post(url, data)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert "username" in response.data
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue