Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • 1.4.1-upgrade-release
  • 1121-download
  • 1218-smartplaylist_backend
  • 1373-login-form-move-reset-your-password-link
  • 1381-progress-bars
  • 1481
  • 1518-update-django-allauth
  • 1645
  • 1675-widget-improperly-configured-missing-resource-id
  • 1675-widget-improperly-configured-missing-resource-id-2
  • 1704-required-props-are-not-always-passed
  • 1716-add-frontend-tests-again
  • 1749-smtp-uri-configuration
  • 1930-first-upload-in-a-batch-always-fails
  • 1976-update-documentation-links-in-readme-files
  • 2054-player-layout
  • 2063-funkwhale-connection-interrupted-every-so-often-requires-network-reset-page-refresh
  • 2091-iii-6-improve-visuals-layout
  • 2151-refused-to-load-spa-manifest-json-2
  • 2154-add-to-playlist-pop-up-hidden-by-now-playing-screen
  • 2155-can-t-see-the-episode-list-of-a-podcast-as-an-anonymous-user-with-anonymous-access-enabled
  • 2156-add-management-command-to-change-file-ref-for-in-place-imported-files-to-s3
  • 2192-clear-queue-bug-when-random-shuffle-is-enabled
  • 2205-channel-page-pagination-link-dont-working
  • 2215-custom-logger-does-not-work-at-all-with-webkit-and-blink-based-browsers
  • 2228-troi-real-world-review
  • 2274-implement-new-upload-api
  • 2303-allow-users-to-own-tagged-items
  • 2395-far-right-filter
  • 2405-front-buttont-trigger-third-party-hook
  • 2408-troi-create-missing-tracks
  • 2416-revert-library-drop
  • 2429-fix-popover-auto-close
  • 2448-complete-tags
  • 2452-fetch-third-party-metadata
  • 2467-fix-radio-builder
  • 2469-Fix-search-bar-in-ManageUploads
  • 2476-deep-upload-links
  • 2480-add-notification-number-badges
  • 2482-upgrade-about-page-to-use-new-ui
  • 2487-fix-accessibility-according-to-WCAG
  • 2490-experiment-use-rstore
  • 2490-experimental-use-simple-data-store
  • 2490-fix-search-modal
  • 2490-search-modal
  • 2501-fix-compatibility-with-older-browsers
  • 2502-drop-uno-and-jquery
  • 2533-allow-followers-in-user-activiy-privacy-level
  • 2539-drop-ansible-installation-method-in-favor-of-docker
  • 2550-22-user-interfaces-for-federation
  • 2560-default-modal-width
  • 623-test
  • 653-enable-starting-embedded-player-at-a-specific-position-in-track
  • activitypub-overview
  • album-sliders
  • arne/2091-improve-visuals
  • back-option-for-edits
  • chore/2406-compose-modularity-scope
  • develop
  • develop-password-reset
  • env-file-cleanup
  • feat/2091-improve-visuals
  • feature/2481-vui-translations
  • fix-amd64-docker-build-gfortran
  • fix-channel-creation
  • fix-front-node-version
  • fix-gitpod
  • fix-plugins-dev-setup
  • fix-rate-limit-serializer
  • fix-schema-channel-metadata-choices
  • flupsi/2803-improve-visuals
  • flupsi/2804-new-upload-process
  • funkwhale-fix_pwa_manifest
  • funkwhale-petitminion-2136-bug-fix-prune-skipped-upload
  • funkwhale-ui-buttons
  • georg/add-typescript
  • gitpod/test-1866
  • global-button-experiment
  • global-buttons
  • juniorjpdj/pkg-repo
  • manage-py-reference
  • merge-review
  • minimal-python-version
  • petitminion-develop-patch-84496
  • pin-mutagen-to-1.46
  • pipenv
  • plugins
  • plugins-v2
  • plugins-v3
  • pre-release/1.3.0
  • prune_skipped_uploads_docs
  • refactor/homepage
  • renovate/front-all-dependencies
  • renovate/front-major-all-dependencies
  • schema-updates
  • small-gitpod-improvements
  • spectacular_schema
  • stable
  • tempArne
  • ui-buttons
  • 0.1
  • 0.10
  • 0.11
  • 0.12
  • 0.13
  • 0.14
  • 0.14.1
  • 0.14.2
  • 0.15
  • 0.16
  • 0.16.1
  • 0.16.2
  • 0.16.3
  • 0.17
  • 0.18
  • 0.18.1
  • 0.18.2
  • 0.18.3
  • 0.19.0
  • 0.19.0-rc1
  • 0.19.0-rc2
  • 0.19.1
  • 0.2
  • 0.2.1
  • 0.2.2
  • 0.2.3
  • 0.2.4
  • 0.2.5
  • 0.2.6
  • 0.20.0
  • 0.20.0-rc1
  • 0.20.1
  • 0.21
  • 0.21-rc1
  • 0.21-rc2
  • 0.21.1
  • 0.21.2
  • 0.3
  • 0.3.1
  • 0.3.2
  • 0.3.3
  • 0.3.4
  • 0.3.5
  • 0.4
  • 0.5
  • 0.5.1
  • 0.5.2
  • 0.5.3
  • 0.5.4
  • 0.6
  • 0.6.1
  • 0.7
  • 0.8
  • 0.9
  • 0.9.1
  • 1.0
  • 1.0-rc1
  • 1.0.1
  • 1.1
  • 1.1-rc1
  • 1.1-rc2
  • 1.1.1
  • 1.1.2
  • 1.1.3
  • 1.1.4
  • 1.2.0
  • 1.2.0-rc1
  • 1.2.0-rc2
  • 1.2.0-testing
  • 1.2.0-testing2
  • 1.2.0-testing3
  • 1.2.0-testing4
  • 1.2.1
  • 1.2.10
  • 1.2.2
  • 1.2.3
  • 1.2.4
  • 1.2.5
  • 1.2.6
  • 1.2.6-1
  • 1.2.7
  • 1.2.8
  • 1.2.9
  • 1.3.0
  • 1.3.0-rc1
  • 1.3.0-rc2
  • 1.3.0-rc3
  • 1.3.0-rc4
  • 1.3.0-rc5
  • 1.3.0-rc6
  • 1.3.1
  • 1.3.2
  • 1.3.3
  • 1.3.4
  • 1.4.0
  • 1.4.0-rc1
  • 1.4.0-rc2
  • 1.4.1
  • 2.0.0-alpha.1
  • 2.0.0-alpha.2
200 results

Target

Select target project
  • funkwhale/funkwhale
  • Luclu7/funkwhale
  • mbothorel/funkwhale
  • EorlBruder/funkwhale
  • tcit/funkwhale
  • JocelynDelalande/funkwhale
  • eneiluj/funkwhale
  • reg/funkwhale
  • ButterflyOfFire/funkwhale
  • m4sk1n/funkwhale
  • wxcafe/funkwhale
  • andybalaam/funkwhale
  • jcgruenhage/funkwhale
  • pblayo/funkwhale
  • joshuaboniface/funkwhale
  • n3ddy/funkwhale
  • gegeweb/funkwhale
  • tohojo/funkwhale
  • emillumine/funkwhale
  • Te-k/funkwhale
  • asaintgenis/funkwhale
  • anoadragon453/funkwhale
  • Sakada/funkwhale
  • ilianaw/funkwhale
  • l4p1n/funkwhale
  • pnizet/funkwhale
  • dante383/funkwhale
  • interfect/funkwhale
  • akhardya/funkwhale
  • svfusion/funkwhale
  • noplanman/funkwhale
  • nykopol/funkwhale
  • roipoussiere/funkwhale
  • Von/funkwhale
  • aurieh/funkwhale
  • icaria36/funkwhale
  • floreal/funkwhale
  • paulwalko/funkwhale
  • comradekingu/funkwhale
  • FurryJulie/funkwhale
  • Legolars99/funkwhale
  • Vierkantor/funkwhale
  • zachhats/funkwhale
  • heyjake/funkwhale
  • sn0w/funkwhale
  • jvoisin/funkwhale
  • gordon/funkwhale
  • Alexander/funkwhale
  • bignose/funkwhale
  • qasim.ali/funkwhale
  • fakegit/funkwhale
  • Kxze/funkwhale
  • stenstad/funkwhale
  • creak/funkwhale
  • Kaze/funkwhale
  • Tixie/funkwhale
  • IISergII/funkwhale
  • lfuelling/funkwhale
  • nhaddag/funkwhale
  • yoasif/funkwhale
  • ifischer/funkwhale
  • keslerm/funkwhale
  • flupe/funkwhale
  • petitminion/funkwhale
  • ariasuni/funkwhale
  • ollie/funkwhale
  • ngaumont/funkwhale
  • techknowlogick/funkwhale
  • Shleeble/funkwhale
  • theflyingfrog/funkwhale
  • jonatron/funkwhale
  • neobrain/funkwhale
  • eorn/funkwhale
  • KokaKiwi/funkwhale
  • u1-liquid/funkwhale
  • marzzzello/funkwhale
  • sirenwatcher/funkwhale
  • newer027/funkwhale
  • codl/funkwhale
  • Zwordi/funkwhale
  • gisforgabriel/funkwhale
  • iuriatan/funkwhale
  • simon/funkwhale
  • bheesham/funkwhale
  • zeoses/funkwhale
  • accraze/funkwhale
  • meliurwen/funkwhale
  • divadsn/funkwhale
  • Etua/funkwhale
  • sdrik/funkwhale
  • Soran/funkwhale
  • kuba-orlik/funkwhale
  • cristianvogel/funkwhale
  • Forceu/funkwhale
  • jeff/funkwhale
  • der_scheibenhacker/funkwhale
  • owlnical/funkwhale
  • jovuit/funkwhale
  • SilverFox15/funkwhale
  • phw/funkwhale
  • mayhem/funkwhale
  • sridhar/funkwhale
  • stromlin/funkwhale
  • rrrnld/funkwhale
  • nitaibezerra/funkwhale
  • jaller94/funkwhale
  • pcouy/funkwhale
  • eduxstad/funkwhale
  • codingHahn/funkwhale
  • captain/funkwhale
  • polyedre/funkwhale
  • leishenailong/funkwhale
  • ccritter/funkwhale
  • lnceballosz/funkwhale
  • fpiesche/funkwhale
  • Fanyx/funkwhale
  • markusblogde/funkwhale
  • Firobe/funkwhale
  • devilcius/funkwhale
  • freaktechnik/funkwhale
  • blopware/funkwhale
  • cone/funkwhale
  • thanksd/funkwhale
  • vachan-maker/funkwhale
  • bbenti/funkwhale
  • tarator/funkwhale
  • prplecake/funkwhale
  • DMarzal/funkwhale
  • lullis/funkwhale
  • hanacgr/funkwhale
  • albjeremias/funkwhale
  • xeruf/funkwhale
  • llelite/funkwhale
  • RoiArthurB/funkwhale
  • cloo/funkwhale
  • nztvar/funkwhale
  • Keunes/funkwhale
  • petitminion/funkwhale-petitminion
  • m-idler/funkwhale
  • SkyLeite/funkwhale
140 results
Select Git revision
  • 170-federation
  • 594-navigation-redesign
  • 735-table-truncate
  • 839-donation-link
  • 865-sql-optimization
  • 890-notification
  • 925-flac-transcoding
  • add-new-shortcuts
  • develop
  • landing-page
  • limit-album-tracks
  • live-streaming
  • master
  • ollie/funkwhale-documentation-fixes
  • plugins
  • plugins-v2
  • vuln-testing
  • webdav
  • 0.1
  • 0.10
  • 0.11
  • 0.12
  • 0.13
  • 0.14
  • 0.14.1
  • 0.14.2
  • 0.15
  • 0.16
  • 0.16.1
  • 0.16.2
  • 0.16.3
  • 0.17
  • 0.18
  • 0.18.1
  • 0.18.2
  • 0.18.3
  • 0.19.0
  • 0.19.0-rc1
  • 0.19.0-rc2
  • 0.19.1
  • 0.2
  • 0.2.1
  • 0.2.2
  • 0.2.3
  • 0.2.4
  • 0.2.5
  • 0.2.6
  • 0.20.0
  • 0.20.0-rc1
  • 0.20.1
  • 0.3
  • 0.3.1
  • 0.3.2
  • 0.3.3
  • 0.3.4
  • 0.3.5
  • 0.4
  • 0.5
  • 0.5.1
  • 0.5.2
  • 0.5.3
  • 0.5.4
  • 0.6
  • 0.6.1
  • 0.7
  • 0.8
  • 0.9
  • 0.9.1
68 results
Show changes
...@@ -3,32 +3,36 @@ import json ...@@ -3,32 +3,36 @@ import json
import pytest import pytest
from django.urls import reverse from django.urls import reverse
from funkwhale_api.favorites.models import TrackFavorite
from funkwhale_api.favorites import serializers from funkwhale_api.favorites import serializers
from funkwhale_api.favorites.models import TrackFavorite
def test_user_can_add_favorite(factories): def test_user_can_add_favorite(factories):
track = factories["music.Track"]() track = factories["music.Track"]()
user = factories["users.User"]() user = factories["users.User"](with_actor=True)
f = TrackFavorite.add(track, user) f = TrackFavorite.add(track, user.actor)
assert f.track == track assert f.track == track
assert f.user == user assert f.actor.user == user
def test_user_can_get_his_favorites( def test_user_can_get_his_favorites(
api_request, factories, logged_in_api_client, client api_request, factories, logged_in_api_client, client
): ):
request = api_request.get("/") request = api_request.get("/")
favorite = factories["favorites.TrackFavorite"](user=logged_in_api_client.user) logged_in_api_client.user.create_actor()
favorite = factories["favorites.TrackFavorite"](
actor=logged_in_api_client.user.actor
)
factories["favorites.TrackFavorite"]() factories["favorites.TrackFavorite"]()
url = reverse("api:v1:favorites:tracks-list") url = reverse("api:v1:favorites:tracks-list")
response = logged_in_api_client.get(url, {"user": logged_in_api_client.user.pk}) response = logged_in_api_client.get(url, {"scope": "me"})
expected = [ expected = [
serializers.UserTrackFavoriteSerializer( serializers.UserTrackFavoriteSerializer(
favorite, context={"request": request} favorite, context={"request": request}
).data ).data
] ]
assert response.status_code == 200 assert response.status_code == 200
assert response.data["results"] == expected assert response.data["results"] == expected
...@@ -36,7 +40,10 @@ def test_user_can_get_his_favorites( ...@@ -36,7 +40,10 @@ def test_user_can_get_his_favorites(
def test_user_can_retrieve_all_favorites_at_once( def test_user_can_retrieve_all_favorites_at_once(
api_request, factories, logged_in_api_client, client api_request, factories, logged_in_api_client, client
): ):
favorite = factories["favorites.TrackFavorite"](user=logged_in_api_client.user) logged_in_api_client.user.create_actor()
favorite = factories["favorites.TrackFavorite"](
actor=logged_in_api_client.user.actor
)
factories["favorites.TrackFavorite"]() factories["favorites.TrackFavorite"]()
url = reverse("api:v1:favorites:tracks-all") url = reverse("api:v1:favorites:tracks-all")
response = logged_in_api_client.get(url, {"user": logged_in_api_client.user.pk}) response = logged_in_api_client.get(url, {"user": logged_in_api_client.user.pk})
...@@ -47,6 +54,8 @@ def test_user_can_retrieve_all_favorites_at_once( ...@@ -47,6 +54,8 @@ def test_user_can_retrieve_all_favorites_at_once(
def test_user_can_add_favorite_via_api(factories, logged_in_api_client, activity_muted): def test_user_can_add_favorite_via_api(factories, logged_in_api_client, activity_muted):
track = factories["music.Track"]() track = factories["music.Track"]()
logged_in_api_client.user.create_actor()
url = reverse("api:v1:favorites:tracks-list") url = reverse("api:v1:favorites:tracks-list")
response = logged_in_api_client.post(url, {"track": track.pk}) response = logged_in_api_client.post(url, {"track": track.pk})
...@@ -60,12 +69,13 @@ def test_user_can_add_favorite_via_api(factories, logged_in_api_client, activity ...@@ -60,12 +69,13 @@ def test_user_can_add_favorite_via_api(factories, logged_in_api_client, activity
assert expected == parsed_json assert expected == parsed_json
assert favorite.track == track assert favorite.track == track
assert favorite.user == logged_in_api_client.user assert favorite.actor.user == logged_in_api_client.user
def test_adding_favorites_calls_activity_record( def test_adding_favorites_calls_activity_record(
factories, logged_in_api_client, activity_muted factories, logged_in_api_client, activity_muted
): ):
logged_in_api_client.user.create_actor()
track = factories["music.Track"]() track = factories["music.Track"]()
url = reverse("api:v1:favorites:tracks-list") url = reverse("api:v1:favorites:tracks-list")
response = logged_in_api_client.post(url, {"track": track.pk}) response = logged_in_api_client.post(url, {"track": track.pk})
...@@ -80,13 +90,16 @@ def test_adding_favorites_calls_activity_record( ...@@ -80,13 +90,16 @@ def test_adding_favorites_calls_activity_record(
assert expected == parsed_json assert expected == parsed_json
assert favorite.track == track assert favorite.track == track
assert favorite.user == logged_in_api_client.user assert favorite.actor.user == logged_in_api_client.user
activity_muted.assert_called_once_with(favorite) activity_muted.assert_called_once_with(favorite)
def test_user_can_remove_favorite_via_api(logged_in_api_client, factories): def test_user_can_remove_favorite_via_api(logged_in_api_client, factories):
favorite = factories["favorites.TrackFavorite"](user=logged_in_api_client.user) logged_in_api_client.user.create_actor()
favorite = factories["favorites.TrackFavorite"](
actor=logged_in_api_client.user.actor
)
url = reverse("api:v1:favorites:tracks-detail", kwargs={"pk": favorite.pk}) url = reverse("api:v1:favorites:tracks-detail", kwargs={"pk": favorite.pk})
response = logged_in_api_client.delete(url, {"track": favorite.track.pk}) response = logged_in_api_client.delete(url, {"track": favorite.track.pk})
assert response.status_code == 204 assert response.status_code == 204
...@@ -97,7 +110,10 @@ def test_user_can_remove_favorite_via_api(logged_in_api_client, factories): ...@@ -97,7 +110,10 @@ def test_user_can_remove_favorite_via_api(logged_in_api_client, factories):
def test_user_can_remove_favorite_via_api_using_track_id( def test_user_can_remove_favorite_via_api_using_track_id(
method, factories, logged_in_api_client method, factories, logged_in_api_client
): ):
favorite = factories["favorites.TrackFavorite"](user=logged_in_api_client.user) logged_in_api_client.user.create_actor()
favorite = factories["favorites.TrackFavorite"](
actor=logged_in_api_client.user.actor
)
url = reverse("api:v1:favorites:tracks-remove") url = reverse("api:v1:favorites:tracks-remove")
response = getattr(logged_in_api_client, method)( response = getattr(logged_in_api_client, method)(
...@@ -117,7 +133,10 @@ def test_url_require_auth(url, method, db, preferences, client): ...@@ -117,7 +133,10 @@ def test_url_require_auth(url, method, db, preferences, client):
def test_can_filter_tracks_by_favorites(factories, logged_in_api_client): def test_can_filter_tracks_by_favorites(factories, logged_in_api_client):
favorite = factories["favorites.TrackFavorite"](user=logged_in_api_client.user) logged_in_api_client.user.create_actor()
favorite = factories["favorites.TrackFavorite"](
actor=logged_in_api_client.user.actor
)
url = reverse("api:v1:tracks-list") url = reverse("api:v1:tracks-list")
response = logged_in_api_client.get(url, data={"favorites": True}) response = logged_in_api_client.get(url, data={"favorites": True})
......
from funkwhale_api.favorites import filters from funkwhale_api.favorites import filters, models
from funkwhale_api.favorites import models
def test_track_favorite_filter_track_artist(factories, mocker, queryset_equal_list): def test_track_favorite_filter_track_artist(factories, mocker, queryset_equal_list):
factories["favorites.TrackFavorite"]() factories["favorites.TrackFavorite"]()
cf = factories["moderation.UserFilter"](for_artist=True) cf = factories["moderation.UserFilter"](for_artist=True)
hidden_fav = factories["favorites.TrackFavorite"](track__artist=cf.target_artist) hidden_fav = factories["favorites.TrackFavorite"](
track__artist_credit__artist=cf.target_artist
)
qs = models.TrackFavorite.objects.all() qs = models.TrackFavorite.objects.all()
filterset = filters.TrackFavoriteFilter( filterset = filters.TrackFavoriteFilter(
{"hidden": "true"}, request=mocker.Mock(user=cf.user), queryset=qs {"hidden": "true"}, request=mocker.Mock(user=cf.user), queryset=qs
...@@ -20,7 +21,7 @@ def test_track_favorite_filter_track_album_artist( ...@@ -20,7 +21,7 @@ def test_track_favorite_filter_track_album_artist(
factories["favorites.TrackFavorite"]() factories["favorites.TrackFavorite"]()
cf = factories["moderation.UserFilter"](for_artist=True) cf = factories["moderation.UserFilter"](for_artist=True)
hidden_fav = factories["favorites.TrackFavorite"]( hidden_fav = factories["favorites.TrackFavorite"](
track__album__artist=cf.target_artist track__album__artist_credit__artist=cf.target_artist
) )
qs = models.TrackFavorite.objects.all() qs = models.TrackFavorite.objects.all()
filterset = filters.TrackFavoriteFilter( filterset = filters.TrackFavoriteFilter(
......
import pytest
from funkwhale_api.favorites import models
@pytest.mark.parametrize(
"privacy_level,expected",
[("me", False), ("instance", True), ("everyone", True)],
)
def test_playable_by_local_actor(privacy_level, expected, factories):
actor = factories["federation.Actor"](local=True)
# default user actor is local
user = factories["users.User"](privacy_level=privacy_level)
user.create_actor()
favorite = factories["favorites.TrackFavorite"](actor=user.actor)
queryset = models.TrackFavorite.objects.all().viewable_by(actor)
match = favorite in list(queryset)
assert match is expected
@pytest.mark.parametrize(
"privacy_level,expected", [("me", False), ("instance", False), ("everyone", True)]
)
def test_not_playable_by_remote_actor(privacy_level, expected, factories):
actor = factories["federation.Actor"]()
# default user actor is local
user = factories["users.User"](privacy_level=privacy_level)
user.create_actor()
favorite = factories["favorites.TrackFavorite"](actor=user.actor)
queryset = models.TrackFavorite.objects.all().viewable_by(actor)
match = favorite in list(queryset)
assert match is expected
from funkwhale_api.federation import serializers as federation_serializers
from funkwhale_api.favorites import serializers from funkwhale_api.favorites import serializers
from funkwhale_api.federation import serializers as federation_serializers
from funkwhale_api.music import serializers as music_serializers from funkwhale_api.music import serializers as music_serializers
from funkwhale_api.users import serializers as users_serializers
def test_track_favorite_serializer(factories, to_api_date): def test_track_favorite_serializer(factories, to_api_date):
favorite = factories["favorites.TrackFavorite"]() favorite = factories["favorites.TrackFavorite"]()
actor = favorite.user.create_actor()
expected = { expected = {
"id": favorite.pk, "id": favorite.pk,
"creation_date": to_api_date(favorite.creation_date), "creation_date": to_api_date(favorite.creation_date),
"track": music_serializers.TrackSerializer(favorite.track).data, "track": music_serializers.TrackSerializer(favorite.track).data,
"actor": federation_serializers.APIActorSerializer(actor).data, "actor": federation_serializers.APIActorSerializer(favorite.actor).data,
"user": users_serializers.UserBasicSerializer(favorite.user).data,
} }
serializer = serializers.UserTrackFavoriteSerializer(favorite) serializer = serializers.UserTrackFavoriteSerializer(favorite)
......
import pytest import pytest
from django.urls import reverse from django.urls import reverse
@pytest.mark.parametrize("level", ["instance", "me", "followers"]) @pytest.mark.parametrize("level", ["instance", "me", "followers"])
def test_privacy_filter(preferences, level, factories, api_client): def test_privacy_filter(preferences, level, factories, api_client):
preferences["common__api_authentication_required"] = False preferences["common__api_authentication_required"] = False
factories["favorites.TrackFavorite"](user__privacy_level=level) user = factories["users.User"](privacy_level=level, with_actor=True)
factories["favorites.TrackFavorite"](actor=user.actor, privacy_level=level)
url = reverse("api:v1:favorites:tracks-list") url = reverse("api:v1:favorites:tracks-list")
response = api_client.get(url) response = api_client.get(url)
assert response.status_code == 200 assert response.status_code == 200
......
import pytest import pytest
import uuid
from django.db.models import Q from django.db.models import Q
from django.urls import reverse from django.urls import reverse
from funkwhale_api.federation import ( from funkwhale_api.federation import (
activity, activity,
models,
api_serializers, api_serializers,
models,
serializers, serializers,
tasks, tasks,
) )
...@@ -20,7 +18,9 @@ def test_receive_validates_basic_attributes_and_stores_activity( ...@@ -20,7 +18,9 @@ def test_receive_validates_basic_attributes_and_stores_activity(
activity.InboxRouter, "get_matching_handlers", return_value=True activity.InboxRouter, "get_matching_handlers", return_value=True
) )
mrf_inbox_registry_apply = mocker.spy(mrf_inbox_registry, "apply") mrf_inbox_registry_apply = mocker.spy(mrf_inbox_registry, "apply")
serializer_init = mocker.spy(serializers.BaseActivitySerializer, "__init__")
mocked_dispatch = mocker.patch("funkwhale_api.common.utils.on_commit") mocked_dispatch = mocker.patch("funkwhale_api.common.utils.on_commit")
inbox_actor = factories["federation.Actor"]()
local_to_actor = factories["users.User"]().create_actor() local_to_actor = factories["users.User"]().create_actor()
local_cc_actor = factories["users.User"]().create_actor() local_cc_actor = factories["users.User"]().create_actor()
remote_actor = factories["federation.Actor"]() remote_actor = factories["federation.Actor"]()
...@@ -33,7 +33,9 @@ def test_receive_validates_basic_attributes_and_stores_activity( ...@@ -33,7 +33,9 @@ def test_receive_validates_basic_attributes_and_stores_activity(
"cc": [local_cc_actor.fid, activity.PUBLIC_ADDRESS], "cc": [local_cc_actor.fid, activity.PUBLIC_ADDRESS],
} }
copy = activity.receive(activity=a, on_behalf_of=remote_actor) copy = activity.receive(
activity=a, on_behalf_of=remote_actor, inbox_actor=inbox_actor
)
mrf_inbox_registry_apply.assert_called_once_with(a, sender_id=a["actor"]) mrf_inbox_registry_apply.assert_called_once_with(a, sender_id=a["actor"])
assert copy.payload == a assert copy.payload == a
...@@ -45,13 +47,46 @@ def test_receive_validates_basic_attributes_and_stores_activity( ...@@ -45,13 +47,46 @@ def test_receive_validates_basic_attributes_and_stores_activity(
tasks.dispatch_inbox.delay, activity_id=copy.pk tasks.dispatch_inbox.delay, activity_id=copy.pk
) )
assert models.InboxItem.objects.count() == 2 assert models.InboxItem.objects.count() == 3
for actor, t in [(local_to_actor, "to"), (local_cc_actor, "cc")]: for actor, t in [
(local_to_actor, "to"),
(inbox_actor, "to"),
(local_cc_actor, "cc"),
]:
ii = models.InboxItem.objects.get(actor=actor) ii = models.InboxItem.objects.get(actor=actor)
assert ii.type == t assert ii.type == t
assert ii.activity == copy assert ii.activity == copy
assert ii.is_read is False assert ii.is_read is False
assert serializer_init.call_args[1]["context"] == {
"actor": remote_actor,
"local_recipients": True,
"recipients": [inbox_actor],
}
assert serializer_init.call_args[1]["data"] == a
def test_receive_uses_follow_object_if_no_audience_provided(
mrf_inbox_registry, factories, now, mocker
):
mocker.patch.object(
activity.InboxRouter, "get_matching_handlers", return_value=True
)
mocker.patch("funkwhale_api.common.utils.on_commit")
local_to_actor = factories["users.User"]().create_actor()
remote_actor = factories["federation.Actor"]()
a = {
"@context": [],
"actor": remote_actor.fid,
"type": "Follow",
"id": "https://test.activity",
"object": local_to_actor.fid,
}
activity.receive(activity=a, on_behalf_of=remote_actor, inbox_actor=None)
assert models.InboxItem.objects.filter(actor=local_to_actor, type="to").exists()
def test_receive_uses_mrf_returned_payload(mrf_inbox_registry, factories, now, mocker): def test_receive_uses_mrf_returned_payload(mrf_inbox_registry, factories, now, mocker):
mocker.patch.object( mocker.patch.object(
...@@ -209,9 +244,6 @@ def test_should_reject(factories, params, policy_kwargs, expected): ...@@ -209,9 +244,6 @@ def test_should_reject(factories, params, policy_kwargs, expected):
def test_get_actors_from_audience_urls(settings, db): def test_get_actors_from_audience_urls(settings, db):
settings.FEDERATION_HOSTNAME = "federation.hostname" settings.FEDERATION_HOSTNAME = "federation.hostname"
library_uuid1 = uuid.uuid4()
library_uuid2 = uuid.uuid4()
urls = [ urls = [
"https://wrong.url", "https://wrong.url",
"https://federation.hostname" "https://federation.hostname"
...@@ -220,21 +252,15 @@ def test_get_actors_from_audience_urls(settings, db): ...@@ -220,21 +252,15 @@ def test_get_actors_from_audience_urls(settings, db):
+ reverse("federation:actors-detail", kwargs={"preferred_username": "alice"}), + reverse("federation:actors-detail", kwargs={"preferred_username": "alice"}),
"https://federation.hostname" "https://federation.hostname"
+ reverse("federation:actors-detail", kwargs={"preferred_username": "bob"}), + reverse("federation:actors-detail", kwargs={"preferred_username": "bob"}),
"https://federation.hostname" "https://federation.hostname",
+ reverse("federation:music:libraries-detail", kwargs={"uuid": library_uuid1}),
"https://federation.hostname"
+ reverse("federation:music:libraries-detail", kwargs={"uuid": library_uuid2}),
activity.PUBLIC_ADDRESS, activity.PUBLIC_ADDRESS,
] ]
followed_query = Q(target__followers_url=urls[0]) followed_query = Q(target__followers_url=urls[0])
for url in urls[1:-1]: for url in urls[1:-1]:
followed_query |= Q(target__followers_url=url) followed_query |= Q(target__followers_url=url)
actor_follows = models.Follow.objects.filter(followed_query, approved=True) actor_follows = models.Follow.objects.filter(followed_query, approved=True)
library_follows = models.LibraryFollow.objects.filter(followed_query, approved=True)
expected = models.Actor.objects.filter( expected = models.Actor.objects.filter(
Q(fid__in=urls[0:-1]) Q(fid__in=urls[0:-1]) | Q(pk__in=actor_follows.values_list("actor", flat=True))
| Q(pk__in=actor_follows.values_list("actor", flat=True))
| Q(pk__in=library_follows.values_list("actor", flat=True))
) )
assert str(activity.get_actors_from_audience(urls).query) == str(expected.query) assert str(activity.get_actors_from_audience(urls).query) == str(expected.query)
...@@ -314,7 +340,7 @@ def test_inbox_routing_send_to_channel(factories, mocker): ...@@ -314,7 +340,7 @@ def test_inbox_routing_send_to_channel(factories, mocker):
ii.refresh_from_db() ii.refresh_from_db()
group_send.assert_called_once_with( group_send.assert_called_once_with(
"user.{}.inbox".format(ii.actor.user.pk), f"user.{ii.actor.user.pk}.inbox",
{ {
"type": "event.send", "type": "event.send",
"text": "", "text": "",
...@@ -417,10 +443,10 @@ def test_outbox_router_dispatch_allow_list(mocker, factories, preferences, now): ...@@ -417,10 +443,10 @@ def test_outbox_router_dispatch_allow_list(mocker, factories, preferences, now):
router.connect({"type": "Noop"}, handler) router.connect({"type": "Noop"}, handler)
router.dispatch({"type": "Noop"}, {"summary": "hello"}) router.dispatch({"type": "Noop"}, {"summary": "hello"})
prepare_deliveries_and_inbox_items.assert_any_call( prepare_deliveries_and_inbox_items.assert_any_call(
[r1], "to", allowed_domains=set([r1.domain_id]) [r1], "to", allowed_domains={r1.domain_id}
) )
prepare_deliveries_and_inbox_items.assert_any_call( prepare_deliveries_and_inbox_items.assert_any_call(
[r2], "cc", allowed_domains=set([r1.domain_id]) [r2], "cc", allowed_domains={r1.domain_id}
) )
...@@ -440,17 +466,10 @@ def test_prepare_deliveries_and_inbox_items(factories, preferences): ...@@ -440,17 +466,10 @@ def test_prepare_deliveries_and_inbox_items(factories, preferences):
shared_inbox_url=remote_actor1.shared_inbox_url shared_inbox_url=remote_actor1.shared_inbox_url
) )
remote_actor3 = factories["federation.Actor"](shared_inbox_url=None) remote_actor3 = factories["federation.Actor"](shared_inbox_url=None)
remote_actor4 = factories["federation.Actor"]()
library = factories["music.Library"]()
library_follower_local = factories["federation.LibraryFollow"](
target=library, actor__local=True, approved=True
).actor
library_follower_remote = factories["federation.LibraryFollow"](
target=library, actor__local=False, approved=True
).actor
# follow not approved # follow not approved
factories["federation.LibraryFollow"]( factories["federation.Follow"](
target=library, actor__local=False, approved=False target=remote_actor3, actor__local=False, approved=False
) )
followed_actor = factories["federation.Actor"]() followed_actor = factories["federation.Actor"]()
...@@ -473,8 +492,8 @@ def test_prepare_deliveries_and_inbox_items(factories, preferences): ...@@ -473,8 +492,8 @@ def test_prepare_deliveries_and_inbox_items(factories, preferences):
remote_actor2, remote_actor2,
remote_actor3, remote_actor3,
activity.PUBLIC_ADDRESS, activity.PUBLIC_ADDRESS,
{"type": "followers", "target": library},
{"type": "followers", "target": followed_actor}, {"type": "followers", "target": followed_actor},
{"type": "actor_inbox", "actor": remote_actor4},
] ]
inbox_items, deliveries, urls = activity.prepare_deliveries_and_inbox_items( inbox_items, deliveries, urls = activity.prepare_deliveries_and_inbox_items(
...@@ -485,7 +504,6 @@ def test_prepare_deliveries_and_inbox_items(factories, preferences): ...@@ -485,7 +504,6 @@ def test_prepare_deliveries_and_inbox_items(factories, preferences):
models.InboxItem(actor=local_actor1, type="to"), models.InboxItem(actor=local_actor1, type="to"),
models.InboxItem(actor=local_actor2, type="to"), models.InboxItem(actor=local_actor2, type="to"),
models.InboxItem(actor=local_actor3, type="to"), models.InboxItem(actor=local_actor3, type="to"),
models.InboxItem(actor=library_follower_local, type="to"),
models.InboxItem(actor=actor_follower_local, type="to"), models.InboxItem(actor=actor_follower_local, type="to"),
], ],
key=lambda v: v.actor.pk, key=lambda v: v.actor.pk,
...@@ -495,7 +513,7 @@ def test_prepare_deliveries_and_inbox_items(factories, preferences): ...@@ -495,7 +513,7 @@ def test_prepare_deliveries_and_inbox_items(factories, preferences):
[ [
models.Delivery(inbox_url=remote_actor1.shared_inbox_url), models.Delivery(inbox_url=remote_actor1.shared_inbox_url),
models.Delivery(inbox_url=remote_actor3.inbox_url), models.Delivery(inbox_url=remote_actor3.inbox_url),
models.Delivery(inbox_url=library_follower_remote.inbox_url), models.Delivery(inbox_url=remote_actor4.inbox_url),
models.Delivery(inbox_url=actor_follower_remote.inbox_url), models.Delivery(inbox_url=actor_follower_remote.inbox_url),
], ],
key=lambda v: v.inbox_url, key=lambda v: v.inbox_url,
...@@ -509,8 +527,8 @@ def test_prepare_deliveries_and_inbox_items(factories, preferences): ...@@ -509,8 +527,8 @@ def test_prepare_deliveries_and_inbox_items(factories, preferences):
remote_actor2.fid, remote_actor2.fid,
remote_actor3.fid, remote_actor3.fid,
activity.PUBLIC_ADDRESS, activity.PUBLIC_ADDRESS,
library.followers_url,
followed_actor.followers_url, followed_actor.followers_url,
remote_actor4.fid,
] ]
assert urls == expected_urls assert urls == expected_urls
...@@ -537,7 +555,7 @@ def test_prepare_deliveries_and_inbox_items_allow_list(factories, preferences): ...@@ -537,7 +555,7 @@ def test_prepare_deliveries_and_inbox_items_allow_list(factories, preferences):
recipients = [remote_actor1, remote_actor2] recipients = [remote_actor1, remote_actor2]
inbox_items, deliveries, urls = activity.prepare_deliveries_and_inbox_items( inbox_items, deliveries, urls = activity.prepare_deliveries_and_inbox_items(
recipients, "to", allowed_domains=set([remote_actor1.domain_id]) recipients, "to", allowed_domains={remote_actor1.domain_id}
) )
expected_inbox_items = [] expected_inbox_items = []
...@@ -556,7 +574,6 @@ def test_prepare_deliveries_and_inbox_items_allow_list(factories, preferences): ...@@ -556,7 +574,6 @@ def test_prepare_deliveries_and_inbox_items_allow_list(factories, preferences):
def test_prepare_deliveries_and_inbox_items_instances_with_followers(factories): def test_prepare_deliveries_and_inbox_items_instances_with_followers(factories):
domain1 = factories["federation.Domain"](with_service_actor=True) domain1 = factories["federation.Domain"](with_service_actor=True)
domain2 = factories["federation.Domain"](with_service_actor=True) domain2 = factories["federation.Domain"](with_service_actor=True)
library = factories["music.Library"](actor__local=True) library = factories["music.Library"](actor__local=True)
......
from funkwhale_api.federation import fields from funkwhale_api.federation import fields, filters, models
from funkwhale_api.federation import filters
from funkwhale_api.federation import models
def test_inbox_item_filter_before(factories): def test_inbox_item_filter_before(factories):
......
import pytest import pytest
from funkwhale_api.federation import api_serializers from funkwhale_api.audio.serializers import ChannelSerializer
from funkwhale_api.federation import serializers from funkwhale_api.common import serializers as common_serializers
from funkwhale_api.federation import api_serializers, serializers
from funkwhale_api.users import serializers as users_serializers
def test_library_serializer(factories, to_api_date): def test_library_serializer(factories, to_api_date):
...@@ -11,7 +13,6 @@ def test_library_serializer(factories, to_api_date): ...@@ -11,7 +13,6 @@ def test_library_serializer(factories, to_api_date):
"uuid": str(library.uuid), "uuid": str(library.uuid),
"actor": serializers.APIActorSerializer(library.actor).data, "actor": serializers.APIActorSerializer(library.actor).data,
"name": library.name, "name": library.name,
"description": library.description,
"creation_date": to_api_date(library.creation_date), "creation_date": to_api_date(library.creation_date),
"uploads_count": library.uploads_count, "uploads_count": library.uploads_count,
"privacy_level": library.privacy_level, "privacy_level": library.privacy_level,
...@@ -34,29 +35,6 @@ def test_library_serializer_latest_scan(factories): ...@@ -34,29 +35,6 @@ def test_library_serializer_latest_scan(factories):
assert serializer.data["latest_scan"] == expected assert serializer.data["latest_scan"] == expected
def test_library_serializer_with_follow(factories, to_api_date):
library = factories["music.Library"](uploads_count=5678)
follow = factories["federation.LibraryFollow"](target=library)
setattr(library, "_follows", [follow])
expected = {
"fid": library.fid,
"uuid": str(library.uuid),
"actor": serializers.APIActorSerializer(library.actor).data,
"name": library.name,
"description": library.description,
"creation_date": to_api_date(library.creation_date),
"uploads_count": library.uploads_count,
"privacy_level": library.privacy_level,
"follow": api_serializers.NestedLibraryFollowSerializer(follow).data,
"latest_scan": None,
}
serializer = api_serializers.LibrarySerializer(library)
assert serializer.data == expected
def test_library_follow_serializer_validates_existing_follow(factories): def test_library_follow_serializer_validates_existing_follow(factories):
follow = factories["federation.LibraryFollow"]() follow = factories["federation.LibraryFollow"]()
serializer = api_serializers.LibraryFollowSerializer( serializer = api_serializers.LibraryFollowSerializer(
...@@ -85,3 +63,145 @@ def test_manage_upload_action_read(factories): ...@@ -85,3 +63,145 @@ def test_manage_upload_action_read(factories):
s.handle_read(ii.__class__.objects.all()) s.handle_read(ii.__class__.objects.all())
assert ii.__class__.objects.filter(is_read=False).count() == 0 assert ii.__class__.objects.filter(is_read=False).count() == 0
@pytest.mark.parametrize(
"factory_name, factory_kwargs, expected",
[
(
"federation.Actor",
{"preferred_username": "hello", "domain__name": "world"},
{"full_username": "hello@world"},
),
(
"music.Library",
{"name": "hello", "uuid": "ad1ee1f7-589c-4abe-b303-e4fe7a889260"},
{"uuid": "ad1ee1f7-589c-4abe-b303-e4fe7a889260", "name": "hello"},
),
(
"federation.LibraryFollow",
{"approved": False, "uuid": "ad1ee1f7-589c-4abe-b303-e4fe7a889260"},
{"uuid": "ad1ee1f7-589c-4abe-b303-e4fe7a889260", "approved": False},
),
],
)
def test_serialize_generic_relation(factory_name, factory_kwargs, expected, factories):
obj = factories[factory_name](**factory_kwargs)
expected["type"] = factory_name
assert api_serializers.serialize_generic_relation({}, obj) == expected
def test_api_full_actor_serializer(factories, to_api_date):
summary = factories["common.Content"]()
icon = factories["common.Attachment"]()
user = factories["users.User"]()
actor = user.create_actor(summary_obj=summary, attachment_icon=icon)
expected = {
"fid": actor.fid,
"url": actor.url,
"creation_date": to_api_date(actor.creation_date),
"last_fetch_date": to_api_date(actor.last_fetch_date),
"user": users_serializers.UserBasicSerializer(user).data,
"is_channel": False,
"domain": actor.domain_id,
"type": actor.type,
"manually_approves_followers": actor.manually_approves_followers,
"full_username": actor.full_username,
"name": actor.name,
"preferred_username": actor.preferred_username,
"is_local": actor.is_local,
"summary": common_serializers.ContentSerializer(summary).data,
"icon": common_serializers.AttachmentSerializer(icon).data,
}
serializer = api_serializers.FullActorSerializer(actor)
assert serializer.data == expected
def test_fetch_serializer_no_obj(factories, to_api_date):
fetch = factories["federation.Fetch"]()
expected = {
"id": fetch.pk,
"url": fetch.url,
"creation_date": to_api_date(fetch.creation_date),
"fetch_date": None,
"status": fetch.status,
"detail": fetch.detail,
"object": None,
"type": None,
"actor": serializers.APIActorSerializer(fetch.actor).data,
}
assert api_serializers.FetchSerializer(fetch).data == expected
@pytest.mark.parametrize(
"object_factory, expected_type, serializer_class",
[
("music.Album", "album", serializers.AlbumSerializer),
("music.Artist", "artist", serializers.ArtistSerializer),
("music.Track", "track", serializers.TrackSerializer),
("audio.Channel", "channel", ChannelSerializer),
("federation.Actor", "account", serializers.APIActorSerializer),
("playlists.Playlist", "playlist", serializers.PlaylistSerializer),
],
)
def test_fetch_serializer_with_object(
object_factory, expected_type, serializer_class, factories, to_api_date
):
obj = factories[object_factory]()
fetch = factories["federation.Fetch"](object=obj)
# Serialize the object
if serializer_class:
object_data = serializer_class(obj).data
else:
object_data = {"uuid": getattr(obj, "uuid", None)}
expected = {
"id": fetch.pk,
"url": fetch.url,
"creation_date": to_api_date(fetch.creation_date),
"fetch_date": None,
"status": fetch.status,
"detail": fetch.detail,
"object": {
**object_data,
},
"type": expected_type,
"actor": serializers.APIActorSerializer(fetch.actor).data,
}
assert api_serializers.FetchSerializer(fetch).data == expected
def test_fetch_serializer_unhandled_obj(factories, to_api_date):
fetch = factories["federation.Fetch"](object=factories["users.User"]())
expected = {
"id": fetch.pk,
"url": fetch.url,
"creation_date": to_api_date(fetch.creation_date),
"fetch_date": None,
"status": fetch.status,
"detail": fetch.detail,
"object": None,
"type": None,
"actor": serializers.APIActorSerializer(fetch.actor).data,
}
assert api_serializers.FetchSerializer(fetch).data == expected
def test_follow_serializer_do_not_allow_already_followed(factories):
actor = factories["federation.Actor"]()
follow = factories["federation.Follow"](actor=actor)
serializer = api_serializers.FollowSerializer(context={"actor": actor})
with pytest.raises(
api_serializers.serializers.ValidationError, match=r"You cannot follow yourself"
):
serializer.validate_target(actor)
with pytest.raises(api_serializers.serializers.ValidationError, match=r"already"):
serializer.validate_target(follow.target)
import pytest import datetime
from unittest.mock import Mock
import pytest
from django.urls import reverse from django.urls import reverse
from funkwhale_api.federation import api_serializers from funkwhale_api.federation import api_serializers, serializers, tasks, views
from funkwhale_api.federation import serializers
from funkwhale_api.federation import views
def test_user_can_list_their_library_follows(factories, logged_in_api_client): def test_user_can_list_their_library_follows(factories, logged_in_api_client):
...@@ -36,9 +36,33 @@ def test_user_can_fetch_library_using_url(mocker, factories, logged_in_api_clien ...@@ -36,9 +36,33 @@ def test_user_can_fetch_library_using_url(mocker, factories, logged_in_api_clien
assert response.data["results"] == [api_serializers.LibrarySerializer(library).data] assert response.data["results"] == [api_serializers.LibrarySerializer(library).data]
def test_user_can_fetch_playlist_library_using_url(
mocker, factories, logged_in_api_client
):
pl_library = factories["music.Library"]()
upload = factories["music.Upload"]()
upload.playlist_libraries.add(pl_library)
mocked_retrieve = mocker.patch(
"funkwhale_api.federation.utils.retrieve_ap_object", return_value=pl_library
)
url = reverse("api:v1:federation:libraries-fetch")
response = logged_in_api_client.post(url, {"fid": pl_library.fid})
assert mocked_retrieve.call_count == 1
args = mocked_retrieve.call_args
assert args[0] == (pl_library.fid,)
assert args[1]["queryset"].model == views.MusicLibraryViewSet.queryset.model
assert args[1]["serializer_class"] == serializers.LibrarySerializer
assert response.status_code == 200
assert response.data["results"] == [
api_serializers.LibrarySerializer(pl_library).data
]
def test_user_can_schedule_library_scan(mocker, factories, logged_in_api_client): def test_user_can_schedule_library_scan(mocker, factories, logged_in_api_client):
actor = logged_in_api_client.user.create_actor() actor = logged_in_api_client.user.actor
library = factories["music.Library"](privacy_level="everyone") library = factories["music.Library"](privacy_level="everyone")
library.actor.user.delete() # make sure library is not local
schedule_scan = mocker.patch( schedule_scan = mocker.patch(
"funkwhale_api.music.models.Library.schedule_scan", return_value=True "funkwhale_api.music.models.Library.schedule_scan", return_value=True
...@@ -94,7 +118,7 @@ def test_user_cannot_edit_someone_else_library_follow( ...@@ -94,7 +118,7 @@ def test_user_cannot_edit_someone_else_library_follow(
logged_in_api_client.user.create_actor() logged_in_api_client.user.create_actor()
follow = factories["federation.LibraryFollow"]() follow = factories["federation.LibraryFollow"]()
url = reverse( url = reverse(
"api:v1:federation:library-follows-{}".format(action), f"api:v1:federation:library-follows-{action}",
kwargs={"uuid": follow.uuid}, kwargs={"uuid": follow.uuid},
) )
response = logged_in_api_client.post(url) response = logged_in_api_client.post(url)
...@@ -112,7 +136,7 @@ def test_user_can_accept_or_reject_own_follows( ...@@ -112,7 +136,7 @@ def test_user_can_accept_or_reject_own_follows(
actor = logged_in_api_client.user.create_actor() actor = logged_in_api_client.user.create_actor()
follow = factories["federation.LibraryFollow"](target__actor=actor) follow = factories["federation.LibraryFollow"](target__actor=actor)
url = reverse( url = reverse(
"api:v1:federation:library-follows-{}".format(action), f"api:v1:federation:library-follows-{action}",
kwargs={"uuid": follow.uuid}, kwargs={"uuid": follow.uuid},
) )
response = logged_in_api_client.post(url) response = logged_in_api_client.post(url)
...@@ -123,12 +147,9 @@ def test_user_can_accept_or_reject_own_follows( ...@@ -123,12 +147,9 @@ def test_user_can_accept_or_reject_own_follows(
assert follow.approved is expected assert follow.approved is expected
if action == "accept":
mocked_dispatch.assert_called_once_with( mocked_dispatch.assert_called_once_with(
{"type": "Accept"}, context={"follow": follow} {"type": action.title()}, context={"follow": follow}
) )
if action == "reject":
mocked_dispatch.assert_not_called()
def test_user_can_list_inbox_items(factories, logged_in_api_client): def test_user_can_list_inbox_items(factories, logged_in_api_client):
...@@ -170,7 +191,8 @@ def test_user_can_update_read_status_of_inbox_item(factories, logged_in_api_clie ...@@ -170,7 +191,8 @@ def test_user_can_update_read_status_of_inbox_item(factories, logged_in_api_clie
def test_can_detail_fetch(logged_in_api_client, factories): def test_can_detail_fetch(logged_in_api_client, factories):
fetch = factories["federation.Fetch"](url="http://test.object") actor = logged_in_api_client.user.create_actor()
fetch = factories["federation.Fetch"](url="http://test.object", actor=actor)
url = reverse("api:v1:federation:fetches-detail", kwargs={"pk": fetch.pk}) url = reverse("api:v1:federation:fetches-detail", kwargs={"pk": fetch.pk})
response = logged_in_api_client.get(url) response = logged_in_api_client.get(url)
...@@ -179,3 +201,277 @@ def test_can_detail_fetch(logged_in_api_client, factories): ...@@ -179,3 +201,277 @@ def test_can_detail_fetch(logged_in_api_client, factories):
assert response.status_code == 200 assert response.status_code == 200
assert response.data == expected assert response.data == expected
def test_user_can_list_domains(factories, api_client, preferences):
preferences["common__api_authentication_required"] = False
allowed = factories["federation.Domain"]()
factories["moderation.InstancePolicy"](
actor=None, for_domain=True, block_all=True
).target_domain
url = reverse("api:v1:federation:domains-list")
response = api_client.get(url)
expected = {
"count": 1,
"next": None,
"previous": None,
"results": [api_serializers.DomainSerializer(allowed).data],
}
assert response.data == expected
def test_can_retrieve_actor(factories, api_client, preferences):
preferences["common__api_authentication_required"] = False
actor = factories["federation.Actor"]()
url = reverse(
"api:v1:federation:actors-detail", kwargs={"full_username": actor.full_username}
)
response = api_client.get(url)
expected = api_serializers.FullActorSerializer(actor).data
assert response.data == expected
def test_can_retrieve_local_actor_with_allow_list_enabled(
factories, api_client, preferences
):
preferences["common__api_authentication_required"] = False
preferences["moderation__allow_list_enabled"] = True
actor = factories["federation.Actor"](local=True)
url = reverse(
"api:v1:federation:actors-detail", kwargs={"full_username": actor.full_username}
)
response = api_client.get(url)
expected = api_serializers.FullActorSerializer(actor).data
assert response.data == expected
@pytest.mark.parametrize(
"object_id, expected_url",
[
("https://fetch.url", "https://fetch.url"),
("name@domain.tld", "webfinger://name@domain.tld"),
("@name@domain.tld", "webfinger://name@domain.tld"),
],
)
def test_can_fetch_using_url_synchronous(
object_id, expected_url, factories, logged_in_api_client, mocker, settings
):
settings.FEDERATION_SYNCHRONOUS_FETCH = True
actor = logged_in_api_client.user.create_actor()
def fake_task(fetch_id):
actor.fetches.filter(id=fetch_id).update(status="finished")
fetch_task = mocker.patch.object(tasks, "fetch", side_effect=fake_task)
url = reverse("api:v1:federation:fetches-list")
data = {"object_uri": object_id}
response = logged_in_api_client.post(url, data)
assert response.status_code == 201
fetch = actor.fetches.latest("id")
assert fetch.status == "finished"
assert fetch.url == expected_url
assert response.data == api_serializers.FetchSerializer(fetch).data
fetch_task.assert_called_once_with(fetch_id=fetch.pk)
def test_fetch_duplicate(factories, logged_in_api_client, settings, now):
object_id = "http://example.test"
settings.FEDERATION_DUPLICATE_FETCH_DELAY = 60
actor = logged_in_api_client.user.create_actor()
duplicate = factories["federation.Fetch"](
actor=actor,
status="finished",
url=object_id,
creation_date=now - datetime.timedelta(seconds=59),
)
url = reverse("api:v1:federation:fetches-list")
data = {"object_uri": object_id}
response = logged_in_api_client.post(url, data)
assert response.status_code == 201
assert response.data == api_serializers.FetchSerializer(duplicate).data
def test_fetch_duplicate_bypass_with_force(
factories, logged_in_api_client, mocker, settings, now
):
fetch_task = mocker.patch.object(tasks, "fetch")
object_id = "http://example.test"
settings.FEDERATION_DUPLICATE_FETCH_DELAY = 60
actor = logged_in_api_client.user.create_actor()
duplicate = factories["federation.Fetch"](
actor=actor,
status="finished",
url=object_id,
creation_date=now - datetime.timedelta(seconds=59),
)
url = reverse("api:v1:federation:fetches-list")
data = {"object_uri": object_id, "force": True}
response = logged_in_api_client.post(url, data)
fetch = actor.fetches.latest("id")
assert fetch != duplicate
assert response.status_code == 201
assert response.data == api_serializers.FetchSerializer(fetch).data
fetch_task.assert_called_once_with(fetch_id=fetch.pk)
def test_library_follow_get_all(factories, logged_in_api_client):
actor = logged_in_api_client.user.create_actor()
library = factories["music.Library"]()
follow = factories["federation.LibraryFollow"](target=library, actor=actor)
factories["federation.LibraryFollow"]()
factories["music.Library"]()
url = reverse("api:v1:federation:library-follows-all")
response = logged_in_api_client.get(url)
assert response.status_code == 200
assert response.data == {
"results": [
{
"uuid": str(follow.uuid),
"library": str(library.uuid),
"approved": follow.approved,
}
],
"count": 1,
}
def test_user_follow_get_all(factories, logged_in_api_client):
actor = logged_in_api_client.user.create_actor()
target_actor = factories["federation.Actor"]()
follow = factories["federation.Follow"](target=target_actor, actor=actor)
factories["federation.Follow"]()
url = reverse("api:v1:federation:user-follows-all")
response = logged_in_api_client.get(url)
assert response.status_code == 200
assert response.data == {
"results": [
{
"uuid": str(follow.uuid),
"actor": str(target_actor.fid),
"approved": follow.approved,
}
],
"count": 1,
}
def test_user_follow_retrieve(factories, logged_in_api_client):
actor = logged_in_api_client.user.create_actor()
target_actor = factories["federation.Actor"]()
follow = factories["federation.Follow"](target=target_actor, actor=actor)
factories["federation.Follow"]()
url = reverse("api:v1:federation:user-follows-detail", kwargs={"uuid": follow.uuid})
response = logged_in_api_client.get(url)
assert response.status_code == 200
def test_user_can_list_their_received_follows(factories, logged_in_api_client):
# followed by someont else
factories["federation.Follow"]()
follow = factories["federation.Follow"](actor__user=logged_in_api_client.user)
url = reverse("api:v1:federation:user-follows-list")
response = logged_in_api_client.get(url)
assert response.data["count"] == 1
assert response.data["results"][0]["uuid"] == str(follow.uuid)
def test_can_follow_user_actor(factories, logged_in_api_client, mocker):
lib = factories["music.Library"]()
lib2 = factories["music.Library"]()
dispatch = mocker.patch("funkwhale_api.federation.routes.outbox.dispatch")
mock_session = Mock()
mock_response = Mock()
# one response for the everyone lib and another one for followers lib
mock_response.json.side_effect = [
{"results": [serializers.LibrarySerializer(lib).data]},
{"results": [serializers.LibrarySerializer(lib2).data]},
]
mock_session.get.return_value = mock_response
mocker.patch(
"funkwhale_api.federation.utils.session.get_session",
return_value=mock_session,
)
actor = logged_in_api_client.user.create_actor()
target_actor = factories["federation.Actor"]()
url = reverse("api:v1:federation:user-follows-list")
lib.delete()
lib2.delete()
response = logged_in_api_client.post(url, {"target": target_actor.fid})
assert response.status_code == 201
follow = target_actor.received_follows.latest("id")
assert follow.approved is None
assert follow.actor == actor
dispatch.assert_any_call({"type": "Follow"}, context={"follow": follow})
def test_can_undo_user_follow(factories, logged_in_api_client, mocker):
dispatch = mocker.patch("funkwhale_api.federation.routes.outbox.dispatch")
actor = logged_in_api_client.user.create_actor()
follow = factories["federation.Follow"](actor=actor)
delete = mocker.patch.object(follow.__class__, "delete")
url = reverse("api:v1:federation:user-follows-detail", kwargs={"uuid": follow.uuid})
response = logged_in_api_client.delete(url)
assert response.status_code == 204
delete.assert_called_once_with()
dispatch.assert_called_once_with(
{"type": "Undo", "object": {"type": "Follow"}}, context={"follow": follow}
)
@pytest.mark.parametrize("action", ["accept", "reject"])
def test_user_cannot_edit_someone_else_user_follow(
factories, logged_in_api_client, action
):
logged_in_api_client.user.create_actor()
follow = factories["federation.Follow"]()
url = reverse(
f"api:v1:federation:user-follows-{action}",
kwargs={"uuid": follow.uuid},
)
response = logged_in_api_client.post(url)
assert response.status_code == 404
@pytest.mark.parametrize("action,expected", [("accept", True), ("reject", False)])
def test_user_can_accept_or_reject_own_received_follows(
factories, logged_in_api_client, action, expected, mocker
):
mocked_dispatch = mocker.patch(
"funkwhale_api.federation.activity.OutboxRouter.dispatch"
)
actor = logged_in_api_client.user.create_actor()
follow = factories["federation.Follow"](target=actor)
url = reverse(
f"api:v1:federation:user-follows-{action}",
kwargs={"uuid": follow.uuid},
)
response = logged_in_api_client.post(url)
assert response.status_code == 204
follow.refresh_from_db()
assert follow.approved is expected
mocked_dispatch.assert_called_once_with(
{"type": action.title()}, context={"follow": follow}
)