import contextlib import io import os import random import shutil import tempfile import time import factory import PIL import pytest from aioresponses import aioresponses from django.contrib.auth.models import AnonymousUser from django.core.cache import cache as django_cache from django.core.cache import caches from django.core.files import uploadedfile from django.core.management import call_command from django.db import connection from django.db.migrations.executor import MigrationExecutor from django.db.models import QuerySet from django.test import client from django.utils import timezone from dynamic_preferences.registries import global_preferences_registry from rest_framework.test import APIClient, APIRequestFactory from funkwhale_api.activity import record from funkwhale_api.federation import actors from funkwhale_api.moderation import mrf from funkwhale_api.music import licenses from . import utils as test_utils pytest_plugins = "aiohttp.pytest_plugin" @pytest.fixture def queryset_equal_queries(): """ Unitting querysets is hard because we have to compare queries by hand. Let's monkey patch querysets to do that for us. """ def __eq__(self, other): if isinstance(other, QuerySet): return str(other.query) == str(self.query) else: return False setattr(QuerySet, "__eq__", __eq__) yield __eq__ delattr(QuerySet, "__eq__") @pytest.fixture def queryset_equal_list(): """ Unitting querysets is hard because we usually simply wants to ensure a querysets contains the same objects as a list, let's monkey patch querysets to to that for us. """ def __eq__(self, other): if isinstance(other, (list, tuple)): return list(self) == list(other) else: return False setattr(QuerySet, "__eq__", __eq__) yield __eq__ delattr(QuerySet, "__eq__") @pytest.fixture(scope="session", autouse=True) def factories_autodiscover(): from django.apps import apps from funkwhale_api import factories app_names = [app.name for app in apps.app_configs.values()] factories.registry.autodiscover(app_names) @pytest.fixture(autouse=True) def cache(): """ Returns a django Cache instance for cache-related operations """ yield django_cache django_cache.clear() if "service_actor" in actors._CACHE: del actors._CACHE["service_actor"] @pytest.fixture(autouse=True) def local_cache(): yield caches["local"] caches["local"].clear() @pytest.fixture def factories(db): """ Returns a dictionary containing all registered factories with keys such as users.User or music.Track """ from funkwhale_api import factories for v in factories.registry.values(): try: v._meta.strategy = factory.CREATE_STRATEGY except AttributeError: # probably not a class based factory pass yield factories.registry @pytest.fixture def nodb_factories(): """ Returns a dictionary containing all registered factories with a build strategy that does not require access to the database """ from funkwhale_api import factories for v in factories.registry.values(): try: v._meta.strategy = factory.BUILD_STRATEGY except AttributeError: # probably not a class based factory pass yield factories.registry @pytest.fixture def preferences(db, cache): """ return a dynamic_preferences manager for global_preferences """ manager = global_preferences_registry.manager() manager.all() yield manager @pytest.fixture def tmpdir(): """ Returns a temporary directory path where you can write things during your test """ d = tempfile.mkdtemp() yield d shutil.rmtree(d) @pytest.fixture def tmpfile(): """ Returns a temporary file where you can write things during your test """ yield tempfile.NamedTemporaryFile() @pytest.fixture def logged_in_client(db, factories, client): """ Returns a logged-in, non-API client with an authenticated ``User`` stored in the ``user`` attribute """ user = factories["users.User"]() assert client.login(username=user.username, password="test") setattr(client, "user", user) yield client delattr(client, "user") @pytest.fixture def anonymous_user(): """Returns a AnonymousUser() instance""" return AnonymousUser() @pytest.fixture def api_client(client): """ Return an API client without any authentication """ return APIClient() @pytest.fixture def logged_in_api_client(db, factories, api_client): """ Return a logged-in API client with an authenticated ``User`` stored in the ``user`` attribute """ user = factories["users.User"]() assert api_client.login(username=user.username, password="test") api_client.force_authenticate(user=user) setattr(api_client, "user", user) yield api_client delattr(api_client, "user") @pytest.fixture def superuser_api_client(db, factories, api_client): """ Return a logged-in API client with an authenticated superuser stored in the ``user`` attribute """ user = factories["users.SuperUser"]() assert api_client.login(username=user.username, password="test") setattr(api_client, "user", user) yield api_client delattr(api_client, "user") @pytest.fixture def superuser_client(db, factories, client): """ Return a logged-in, non-API client with an authenticated ``User`` stored in the ``user`` attribute """ user = factories["users.SuperUser"]() assert client.login(username=user.username, password="test") setattr(client, "user", user) yield client delattr(client, "user") @pytest.fixture def api_request(): """ Returns a dummy API request object you can pass to API views """ return APIRequestFactory() @pytest.fixture def fake_request(): """ Returns a dummy, non-API request object you can pass to regular views """ return client.RequestFactory() @pytest.fixture def activity_registry(): state = list(record.registry.items()) yield record.registry record.registry.clear() for key, value in state: record.registry[key] = value @pytest.fixture def activity_muted(activity_registry, mocker): yield mocker.patch.object(record, "send") @pytest.fixture(autouse=True) def media_root(settings): """ Sets settings.MEDIA_ROOT to a temporary path and returns this path """ tmp_dir = tempfile.mkdtemp() settings.MEDIA_ROOT = tmp_dir yield settings.MEDIA_ROOT shutil.rmtree(tmp_dir) @pytest.fixture(autouse=True) def disabled_musicbrainz(mocker): # we ensure no music brainz requests gets out yield mocker.patch( "musicbrainzngs.musicbrainz._safe_read", side_effect=Exception("Disabled network calls"), ) @pytest.fixture(autouse=True) def r_mock(requests_mock): """ Returns a requests_mock.mock() object you can use to mock HTTP calls made using python-requests """ yield requests_mock @pytest.fixture def authenticated_actor(factories, mocker): """ Returns an authenticated ActivityPub actor """ actor = factories["federation.Actor"]() mocker.patch( "funkwhale_api.federation.authentication.SignatureAuthentication.authenticate_actor", return_value=actor, ) yield actor @pytest.fixture(scope="session") def to_api_date(): return test_utils.to_api_date @pytest.fixture() def now(mocker): now = timezone.now() mocker.patch("django.utils.timezone.now", return_value=now) return now @pytest.fixture() def now_time(mocker): now = time.time() mocker.patch("time.time", return_value=now) return now @pytest.fixture() def avatar(): i = PIL.Image.new("RGBA", (400, 400), random.choice(["red", "blue", "yellow"])) f = io.BytesIO() i.save(f, "png") f.name = "avatar.png" f.seek(0) yield f f.close() @pytest.fixture() def audio_file(): data_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "music") path = os.path.join(data_dir, "test.ogg") assert os.path.exists(path) with open(path, "rb") as f: yield f @pytest.fixture() def uploaded_audio_file(audio_file): yield uploadedfile.SimpleUploadedFile( name=audio_file.name, content=audio_file.read() ) @pytest.fixture() def temp_signal(mocker): """ Connect a temporary handler to a given signal. This is helpful to validate a signal is dispatched properly, without mocking. """ @contextlib.contextmanager def connect(signal): stub = mocker.stub() signal.connect(stub) try: yield stub finally: signal.disconnect(stub) return connect @pytest.fixture() def stdout(): yield io.StringIO() @pytest.fixture def spa_html(r_mock, settings): settings.FUNKWHALE_SPA_HTML_ROOT = "http://noop/" yield r_mock.get( settings.FUNKWHALE_SPA_HTML_ROOT + "index.html", text="" ) @pytest.fixture def no_api_auth(preferences): preferences["common__api_authentication_required"] = False @pytest.fixture() def migrator(transactional_db): yield MigrationExecutor(connection) call_command("migrate", interactive=False) @pytest.fixture(autouse=True) def rsa_small_key(settings): # smaller size for faster generation, since it's CPU hungry settings.RSA_KEY_SIZE = 512 @pytest.fixture(autouse=True) def a_responses(): with aioresponses() as m: yield m @pytest.fixture def service_actor(db): return actors.get_service_actor() @pytest.fixture def mrf_inbox_registry(mocker): registry = mrf.Registry() mocker.patch("funkwhale_api.moderation.mrf.inbox", registry) return registry @pytest.fixture def mrf_outbox_registry(mocker): registry = mrf.Registry() mocker.patch("funkwhale_api.moderation.mrf.outbox", registry) return registry @pytest.fixture(autouse=True) def clear_license_cache(db): licenses._cache = None yield licenses._cache = None @pytest.fixture def faker(): return factory.Faker._get_faker()