Skip to content
Snippets Groups Projects
Verified Commit c8a2ae42 authored by Eliot Berriot's avatar Eliot Berriot
Browse files

Merge branch 'release/0.3'

parents 86c607de 7bbb7387
No related branches found
No related tags found
No related merge requests found
Showing
with 70 additions and 571 deletions
......@@ -47,16 +47,15 @@ class TagViewSetMixin(object):
class ArtistViewSet(SearchMixin, viewsets.ReadOnlyModelViewSet):
queryset = (
models.Artist.objects.all()
.order_by('name')
.prefetch_related(
'albums__tracks__files',
'albums__tracks__artist',
'albums__tracks__tags'))
serializer_class = serializers.ArtistSerializerNested
permission_classes = [ConditionalAuthentication]
search_fields = ['name']
ordering_fields = ('creation_date', 'name')
filter_class = filters.ArtistFilter
ordering_fields = ('id', 'name', 'creation_date')
class AlbumViewSet(SearchMixin, viewsets.ReadOnlyModelViewSet):
queryset = (
......@@ -96,7 +95,12 @@ class TrackViewSet(TagViewSetMixin, SearchMixin, viewsets.ReadOnlyModelViewSet):
serializer_class = serializers.TrackSerializerNested
permission_classes = [ConditionalAuthentication]
search_fields = ['title', 'artist__name']
ordering_fields = ('creation_date',)
ordering_fields = (
'creation_date',
'title',
'album__title',
'artist__name',
)
def get_queryset(self):
queryset = super().get_queryset()
......
import json
import unittest
from test_plus.test import TestCase
from django.urls import reverse
from funkwhale_api.musicbrainz import api
from . import data as api_data
class TestAPI(TestCase):
@unittest.mock.patch(
'funkwhale_api.musicbrainz.api.recordings.search',
return_value=api_data.recordings['search']['brontide matador'])
def test_can_search_recording_in_musicbrainz_api(self, *mocks):
query = 'brontide matador'
url = reverse('api:v1:providers:musicbrainz:search-recordings')
expected = api_data.recordings['search']['brontide matador']
response = self.client.get(url, data={'query': query})
self.assertEqual(expected, json.loads(response.content.decode('utf-8')))
@unittest.mock.patch(
'funkwhale_api.musicbrainz.api.releases.search',
return_value=api_data.releases['search']['brontide matador'])
def test_can_search_release_in_musicbrainz_api(self, *mocks):
query = 'brontide matador'
url = reverse('api:v1:providers:musicbrainz:search-releases')
expected = api_data.releases['search']['brontide matador']
response = self.client.get(url, data={'query': query})
self.assertEqual(expected, json.loads(response.content.decode('utf-8')))
@unittest.mock.patch(
'funkwhale_api.musicbrainz.api.artists.search',
return_value=api_data.artists['search']['lost fingers'])
def test_can_search_artists_in_musicbrainz_api(self, *mocks):
query = 'lost fingers'
url = reverse('api:v1:providers:musicbrainz:search-artists')
expected = api_data.artists['search']['lost fingers']
response = self.client.get(url, data={'query': query})
self.assertEqual(expected, json.loads(response.content.decode('utf-8')))
@unittest.mock.patch(
'funkwhale_api.musicbrainz.api.artists.get',
return_value=api_data.artists['get']['lost fingers'])
def test_can_get_artist_in_musicbrainz_api(self, *mocks):
uuid = 'ac16bbc0-aded-4477-a3c3-1d81693d58c9'
url = reverse('api:v1:providers:musicbrainz:artist-detail', kwargs={
'uuid': uuid,
})
response = self.client.get(url)
expected = api_data.artists['get']['lost fingers']
self.assertEqual(expected, json.loads(response.content.decode('utf-8')))
@unittest.mock.patch(
'funkwhale_api.musicbrainz.api.release_groups.browse',
return_value=api_data.release_groups['browse']['lost fingers'])
def test_can_broswe_release_group_using_musicbrainz_api(self, *mocks):
uuid = 'ac16bbc0-aded-4477-a3c3-1d81693d58c9'
url = reverse(
'api:v1:providers:musicbrainz:release-group-browse',
kwargs={
'artist_uuid': uuid,
}
)
response = self.client.get(url)
expected = api_data.release_groups['browse']['lost fingers']
self.assertEqual(expected, json.loads(response.content.decode('utf-8')))
@unittest.mock.patch(
'funkwhale_api.musicbrainz.api.releases.browse',
return_value=api_data.releases['browse']['Lost in the 80s'])
def test_can_broswe_releases_using_musicbrainz_api(self, *mocks):
uuid = 'f04ed607-11b7-3843-957e-503ecdd485d1'
url = reverse(
'api:v1:providers:musicbrainz:release-browse',
kwargs={
'release_group_uuid': uuid,
}
)
response = self.client.get(url)
expected = api_data.releases['browse']['Lost in the 80s']
self.assertEqual(expected, json.loads(response.content.decode('utf-8')))
import unittest
from test_plus.test import TestCase
from funkwhale_api.musicbrainz import client
class TestAPI(TestCase):
def test_can_search_recording_in_musicbrainz_api(self, *mocks):
r = {'hello': 'world'}
mocked = 'funkwhale_api.musicbrainz.client._api.search_artists'
with unittest.mock.patch(mocked, return_value=r) as m:
self.assertEqual(client.api.artists.search('test'), r)
# now call from cache
self.assertEqual(client.api.artists.search('test'), r)
self.assertEqual(client.api.artists.search('test'), r)
self.assertEqual(m.call_count, 1)
import factory
from funkwhale_api.factories import registry
from funkwhale_api.users.factories import UserFactory
@registry.register
class PlaylistFactory(factory.django.DjangoModelFactory):
name = factory.Faker('name')
user = factory.SubFactory(UserFactory)
class Meta:
model = 'playlists.Playlist'
import json
from test_plus.test import TestCase
from django.urls import reverse
from django.core.exceptions import ValidationError
from django.utils import timezone
from funkwhale_api.music.tests import factories
from funkwhale_api.users.models import User
from funkwhale_api.playlists import models
from funkwhale_api.playlists.serializers import PlaylistSerializer
class TestPlayLists(TestCase):
def setUp(self):
super().setUp()
self.user = User.objects.create_user(username='test', email='test@test.com', password='test')
def test_can_create_playlist(self):
tracks = factories.TrackFactory.create_batch(size=5)
playlist = models.Playlist.objects.create(user=self.user, name="test")
previous = None
for i in range(len(tracks)):
previous = playlist.add_track(tracks[i], previous=previous)
playlist_tracks = list(playlist.playlist_tracks.all())
previous = None
for idx, track in enumerate(tracks):
plt = playlist_tracks[idx]
self.assertEqual(plt.position, idx)
self.assertEqual(plt.track, track)
if previous:
self.assertEqual(playlist_tracks[idx + 1], previous)
self.assertEqual(plt.playlist, playlist)
def test_can_create_playlist_via_api(self):
self.client.login(username=self.user.username, password='test')
url = reverse('api:v1:playlists-list')
data = {
'name': 'test',
}
response = self.client.post(url, data)
playlist = self.user.playlists.latest('id')
self.assertEqual(playlist.name, 'test')
def test_can_add_playlist_track_via_api(self):
tracks = factories.TrackFactory.create_batch(size=5)
playlist = models.Playlist.objects.create(user=self.user, name="test")
self.client.login(username=self.user.username, password='test')
url = reverse('api:v1:playlist-tracks-list')
data = {
'playlist': playlist.pk,
'track': tracks[0].pk
}
response = self.client.post(url, data)
plts = self.user.playlists.latest('id').playlist_tracks.all()
self.assertEqual(plts.first().track, tracks[0])
import os
import datetime
import unittest
from test_plus.test import TestCase
from funkwhale_api.providers.audiofile import tasks
DATA_DIR = os.path.dirname(os.path.abspath(__file__))
class TestAudioFile(TestCase):
def test_can_import_single_audio_file(self, *mocks):
metadata = {
'artist': ['Test artist'],
'album': ['Test album'],
'title': ['Test track'],
'TRACKNUMBER': ['4'],
'date': ['2012-08-15'],
'musicbrainz_albumid': ['a766da8b-8336-47aa-a3ee-371cc41ccc75'],
'musicbrainz_trackid': ['bd21ac48-46d8-4e78-925f-d9cc2a294656'],
'musicbrainz_artistid': ['013c8e5b-d72a-4cd3-8dee-6c64d6125823'],
}
m1 = unittest.mock.patch('mutagen.File', return_value=metadata)
m2 = unittest.mock.patch(
'funkwhale_api.music.metadata.Metadata.get_file_type',
return_value='OggVorbis',
)
with m1, m2:
track_file = tasks.from_path(
os.path.join(DATA_DIR, 'dummy_file.ogg'))
self.assertEqual(
track_file.track.title, metadata['title'][0])
self.assertEqual(
track_file.track.mbid, metadata['musicbrainz_trackid'][0])
self.assertEqual(
track_file.track.position, 4)
self.assertEqual(
track_file.track.album.title, metadata['album'][0])
self.assertEqual(
track_file.track.album.mbid,
metadata['musicbrainz_albumid'][0])
self.assertEqual(
track_file.track.album.release_date, datetime.date(2012, 8, 15))
self.assertEqual(
track_file.track.artist.name, metadata['artist'][0])
self.assertEqual(
track_file.track.artist.mbid,
metadata['musicbrainz_artistid'][0])
import json
from collections import OrderedDict
import unittest
from test_plus.test import TestCase
from django.urls import reverse
from funkwhale_api.providers.youtube.client import client
from . import data as api_data
class TestAPI(TestCase):
maxDiff = None
@unittest.mock.patch(
'funkwhale_api.providers.youtube.client._do_search',
return_value=api_data.search['8 bit adventure'])
def test_can_get_search_results_from_youtube(self, *mocks):
query = '8 bit adventure'
results = client.search(query)
self.assertEqual(results[0]['id']['videoId'], '0HxZn6CzOIo')
self.assertEqual(results[0]['snippet']['title'], 'AdhesiveWombat - 8 Bit Adventure')
self.assertEqual(results[0]['full_url'], 'https://www.youtube.com/watch?v=0HxZn6CzOIo')
@unittest.mock.patch(
'funkwhale_api.providers.youtube.client._do_search',
return_value=api_data.search['8 bit adventure'])
def test_can_get_search_results_from_funkwhale(self, *mocks):
query = '8 bit adventure'
url = self.reverse('api:v1:providers:youtube:search')
response = self.client.get(url + '?query={0}'.format(query))
# we should cast the youtube result to something more generic
expected = {
"id": "0HxZn6CzOIo",
"url": "https://www.youtube.com/watch?v=0HxZn6CzOIo",
"type": "youtube#video",
"description": "Make sure to apply adhesive evenly before use. GET IT HERE: http://adhesivewombat.bandcamp.com/album/marsupial-madness Facebook: ...",
"channelId": "UCps63j3krzAG4OyXeEyuhFw",
"title": "AdhesiveWombat - 8 Bit Adventure",
"channelTitle": "AdhesiveWombat",
"publishedAt": "2012-08-22T18:41:03.000Z",
"cover": "https://i.ytimg.com/vi/0HxZn6CzOIo/hqdefault.jpg"
}
self.assertEqual(
json.loads(response.content.decode('utf-8'))[0], expected)
@unittest.mock.patch(
'funkwhale_api.providers.youtube.client._do_search',
side_effect=[
api_data.search['8 bit adventure'],
api_data.search['system of a down toxicity'],
]
)
def test_can_send_multiple_queries_at_once(self, *mocks):
queries = OrderedDict()
queries['1'] = {
'q': '8 bit adventure',
}
queries['2'] = {
'q': 'system of a down toxicity',
}
results = client.search_multiple(queries)
self.assertEqual(results['1'][0]['id']['videoId'], '0HxZn6CzOIo')
self.assertEqual(results['1'][0]['snippet']['title'], 'AdhesiveWombat - 8 Bit Adventure')
self.assertEqual(results['1'][0]['full_url'], 'https://www.youtube.com/watch?v=0HxZn6CzOIo')
self.assertEqual(results['2'][0]['id']['videoId'], 'BorYwGi2SJc')
self.assertEqual(results['2'][0]['snippet']['title'], 'System of a Down: Toxicity')
self.assertEqual(results['2'][0]['full_url'], 'https://www.youtube.com/watch?v=BorYwGi2SJc')
@unittest.mock.patch(
'funkwhale_api.providers.youtube.client._do_search',
return_value=api_data.search['8 bit adventure'],
)
def test_can_send_multiple_queries_at_once_from_funwkhale(self, *mocks):
queries = OrderedDict()
queries['1'] = {
'q': '8 bit adventure',
}
expected = {
"id": "0HxZn6CzOIo",
"url": "https://www.youtube.com/watch?v=0HxZn6CzOIo",
"type": "youtube#video",
"description": "Make sure to apply adhesive evenly before use. GET IT HERE: http://adhesivewombat.bandcamp.com/album/marsupial-madness Facebook: ...",
"channelId": "UCps63j3krzAG4OyXeEyuhFw",
"title": "AdhesiveWombat - 8 Bit Adventure",
"channelTitle": "AdhesiveWombat",
"publishedAt": "2012-08-22T18:41:03.000Z",
"cover": "https://i.ytimg.com/vi/0HxZn6CzOIo/hqdefault.jpg"
}
url = self.reverse('api:v1:providers:youtube:searchs')
response = self.client.post(
url, json.dumps(queries), content_type='application/json')
self.assertEqual(
expected,
json.loads(response.content.decode('utf-8'))['1'][0])
import random
import json
from test_plus.test import TestCase
from django.urls import reverse
from django.core.exceptions import ValidationError
from funkwhale_api.radios import radios
from funkwhale_api.radios import models
from funkwhale_api.favorites.models import TrackFavorite
from funkwhale_api.users.models import User
from funkwhale_api.music.models import Artist
from funkwhale_api.music.tests import factories
from funkwhale_api.history.tests.factories import ListeningFactory
class TestRadios(TestCase):
def setUp(self):
super().setUp()
self.user = User.objects.create_user(username='test', email='test@test.com', password='test')
def test_can_pick_track_from_choices(self):
choices = [1, 2, 3, 4, 5]
radio = radios.SimpleRadio()
first_pick = radio.pick(choices=choices)
self.assertIn(first_pick, choices)
previous_choices = [first_pick]
for remaining_choice in choices:
pick = radio.pick(choices=choices, previous_choices=previous_choices)
self.assertIn(pick, set(choices).difference(previous_choices))
def test_can_pick_by_weight(self):
choices_with_weight = [
# choice, weight
(1, 1),
(2, 2),
(3, 3),
(4, 4),
(5, 5),
]
picks = {choice: 0 for choice, weight in choices_with_weight}
for i in range(1000):
radio = radios.SimpleRadio()
pick = radio.weighted_pick(choices=choices_with_weight)
picks[pick] = picks[pick] + 1
self.assertTrue(picks[5] > picks[4])
self.assertTrue(picks[4] > picks[3])
self.assertTrue(picks[3] > picks[2])
self.assertTrue(picks[2] > picks[1])
def test_can_get_choices_for_favorites_radio(self):
tracks = factories.TrackFactory.create_batch(size=100)
for i in range(20):
TrackFavorite.add(track=random.choice(tracks), user=self.user)
radio = radios.FavoritesRadio()
choices = radio.get_choices(user=self.user)
self.assertEqual(choices.count(), self.user.track_favorites.all().count())
for favorite in self.user.track_favorites.all():
self.assertIn(favorite.track, choices)
for i in range(20):
pick = radio.pick(user=self.user)
self.assertIn(pick, choices)
def test_can_use_radio_session_to_filter_choices(self):
tracks = factories.TrackFactory.create_batch(size=30)
radio = radios.RandomRadio()
session = radio.start_session(self.user)
for i in range(30):
p = radio.pick()
# ensure 30 differents tracks have been suggested
tracks_id = [session_track.track.pk for session_track in session.session_tracks.all()]
self.assertEqual(len(set(tracks_id)), 30)
def test_can_restore_radio_from_previous_session(self):
tracks = factories.TrackFactory.create_batch(size=30)
radio = radios.RandomRadio()
session = radio.start_session(self.user)
restarted_radio = radios.RandomRadio(session)
self.assertEqual(radio.session, restarted_radio.session)
def test_can_get_start_radio_from_api(self):
url = reverse('api:v1:radios:sessions-list')
response = self.client.post(url, {'radio_type': 'random'})
session = models.RadioSession.objects.latest('id')
self.assertEqual(session.radio_type, 'random')
self.assertEqual(session.user, None)
self.client.login(username=self.user.username, password='test')
response = self.client.post(url, {'radio_type': 'random'})
session = models.RadioSession.objects.latest('id')
self.assertEqual(session.radio_type, 'random')
self.assertEqual(session.user, self.user)
def test_can_start_radio_for_anonymous_user(self):
url = reverse('api:v1:radios:sessions-list')
response = self.client.post(url, {'radio_type': 'random'})
session = models.RadioSession.objects.latest('id')
self.assertIsNone(session.user)
self.assertIsNotNone(session.session_key)
def test_can_get_track_for_session_from_api(self):
tracks = factories.TrackFactory.create_batch(size=1)
self.client.login(username=self.user.username, password='test')
url = reverse('api:v1:radios:sessions-list')
response = self.client.post(url, {'radio_type': 'random'})
session = models.RadioSession.objects.latest('id')
url = reverse('api:v1:radios:tracks-list')
response = self.client.post(url, {'session': session.pk})
data = json.loads(response.content.decode('utf-8'))
self.assertEqual(data['track']['id'], tracks[0].id)
self.assertEqual(data['position'], 1)
next_track = factories.TrackFactory()
response = self.client.post(url, {'session': session.pk})
data = json.loads(response.content.decode('utf-8'))
self.assertEqual(data['track']['id'], next_track.id)
self.assertEqual(data['position'], 2)
def test_related_object_radio_validate_related_object(self):
# cannot start without related object
radio = radios.ArtistRadio()
with self.assertRaises(ValidationError):
radio.start_session(self.user)
# cannot start with bad related object type
radio = radios.ArtistRadio()
with self.assertRaises(ValidationError):
radio.start_session(self.user, related_object=self.user)
def test_can_start_artist_radio(self):
artist = factories.ArtistFactory()
wrong_tracks = factories.TrackFactory.create_batch(size=30)
good_tracks = factories.TrackFactory.create_batch(
artist=artist, size=5)
radio = radios.ArtistRadio()
session = radio.start_session(self.user, related_object=artist)
self.assertEqual(session.radio_type, 'artist')
for i in range(5):
self.assertIn(radio.pick(), good_tracks)
def test_can_start_tag_radio(self):
tag = factories.TagFactory()
wrong_tracks = factories.TrackFactory.create_batch(size=30)
good_tracks = factories.TrackFactory.create_batch(size=5)
for track in good_tracks:
track.tags.add(tag)
radio = radios.TagRadio()
session = radio.start_session(self.user, related_object=tag)
self.assertEqual(session.radio_type, 'tag')
for i in range(5):
self.assertIn(radio.pick(), good_tracks)
def test_can_start_artist_radio_from_api(self):
artist = factories.ArtistFactory()
url = reverse('api:v1:radios:sessions-list')
response = self.client.post(url, {'radio_type': 'artist', 'related_object_id': artist.id})
session = models.RadioSession.objects.latest('id')
self.assertEqual(session.radio_type, 'artist')
self.assertEqual(session.related_object, artist)
def test_can_start_less_listened_radio(self):
history = ListeningFactory.create_batch(size=5, user=self.user)
wrong_tracks = [h.track for h in history]
good_tracks = factories.TrackFactory.create_batch(size=30)
radio = radios.LessListenedRadio()
session = radio.start_session(self.user)
self.assertEqual(session.related_object, self.user)
for i in range(5):
self.assertIn(radio.pick(), good_tracks)
import factory
from funkwhale_api.factories import registry
from django.contrib.auth.models import Permission
@registry.register
class UserFactory(factory.django.DjangoModelFactory):
username = factory.Sequence(lambda n: 'user-{0}'.format(n))
email = factory.Sequence(lambda n: 'user-{0}@example.com'.format(n))
password = factory.PostGenerationMethodCall('set_password', 'password')
password = factory.PostGenerationMethodCall('set_password', 'test')
class Meta:
model = 'users.User'
......@@ -28,3 +30,9 @@ class UserFactory(factory.django.DjangoModelFactory):
]
# A list of permissions were passed in, use them
self.user_permissions.add(*perms)
@registry.register(name='users.SuperUser')
class SuperUserFactory(UserFactory):
is_staff = True
is_superuser = True
# Generated by Django 2.0 on 2017-12-26 13:57
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
dependencies = [
('users', '0002_auto_20171214_2205'),
]
operations = [
migrations.AddField(
model_name='user',
name='secret_key',
field=models.UUIDField(default=uuid.uuid4, null=True),
),
migrations.AlterField(
model_name='user',
name='last_name',
field=models.CharField(blank=True, max_length=150, verbose_name='last name'),
),
]
# -*- coding: utf-8 -*-
from __future__ import unicode_literals, absolute_import
import uuid
from django.contrib.auth.models import AbstractUser
from django.urls import reverse
from django.db import models
......@@ -15,6 +17,8 @@ class User(AbstractUser):
# around the globe.
name = models.CharField(_("Name of User"), blank=True, max_length=255)
# updated on logout or password change, to invalidate JWT
secret_key = models.UUIDField(default=uuid.uuid4, null=True)
# permissions that are used for API access and that worth serializing
relevant_permissions = {
# internal_codename : {external_codename}
......@@ -31,3 +35,11 @@ class User(AbstractUser):
def get_absolute_url(self):
return reverse('users:detail', kwargs={'username': self.username})
def update_secret_key(self):
self.secret_key = uuid.uuid4()
return self.secret_key
def set_password(self, raw_password):
super().set_password(raw_password)
self.update_secret_key()
......@@ -2,11 +2,15 @@ from django.views.generic import TemplateView
from django.conf.urls import url
from rest_auth.registration.views import VerifyEmailView
from rest_auth.views import PasswordChangeView
from .views import RegisterView
urlpatterns = [
url(r'^$', RegisterView.as_view(), name='rest_register'),
url(r'^verify-email/$', VerifyEmailView.as_view(), name='rest_verify_email'),
url(r'^change-password/$', PasswordChangeView.as_view(), name='change_password'),
# This url is used by django-allauth and empty TemplateView is
# defined just to allow reverse() call inside app, for example when email
......
from test_plus.test import TestCase
from ..admin import MyUserCreationForm
class TestMyUserCreationForm(TestCase):
def setUp(self):
self.user = self.make_user()
def test_clean_username_success(self):
# Instantiate the form with a new username
form = MyUserCreationForm({
'username': 'alamode',
'password1': '123456',
'password2': '123456',
})
# Run is_valid() to trigger the validation
valid = form.is_valid()
self.assertTrue(valid)
# Run the actual clean_username method
username = form.clean_username()
self.assertEqual('alamode', username)
def test_clean_username_false(self):
# Instantiate the form with the same username as self.user
form = MyUserCreationForm({
'username': self.user.username,
'password1': '123456',
'password2': '123456',
})
# Run is_valid() to trigger the validation, which is going to fail
# because the username is already taken
valid = form.is_valid()
self.assertFalse(valid)
# The form.errors dict should contain a single error called 'username'
self.assertTrue(len(form.errors) == 1)
self.assertTrue('username' in form.errors)
from test_plus.test import TestCase
class TestUser(TestCase):
def setUp(self):
self.user = self.make_user()
def test__str__(self):
self.assertEqual(
self.user.__str__(),
"testuser" # This is the default username for self.make_user()
)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment