import itertools import json from datetime import timedelta import pytest from django.conf import settings from django.contrib import auth from django.urls import reverse from django.utils import timezone from django_scopes import scope from cookbook.models import Recipe, SearchFields from cookbook.tests.conftest import transpose from cookbook.tests.factories import (CookLogFactory, FoodFactory, IngredientFactory, KeywordFactory, RecipeBookEntryFactory, RecipeFactory, UnitFactory, ViewLogFactory) # TODO test combining any/all of the above # TODO test sort_by # TODO test sort_by new X number of recipes are new within last Y days # TODO test loading custom filter # TODO test loading custom filter with overrided params # TODO makenow with above filters # TODO test search food/keywords including/excluding children LIST_URL = 'api:recipe-list' sqlite = settings.DATABASES['default']['ENGINE'] != 'django.db.backends.postgresql' @pytest.fixture def accent(): return "àbçđêf ğĦìĵķĽmñ öPqŕşŧ úvŵxyž" @pytest.fixture def unaccent(): return "abcdef ghijklmn opqrst uvwxyz" @pytest.fixture def user1(request, space_1, u1_s1, unaccent): user = auth.get_user(u1_s1) try: params = {x[0]: x[1] for x in request.param} except AttributeError: params = {} result = 1 misspelled_result = 0 search_term = unaccent if params.get('fuzzy_lookups', False): user.searchpreference.lookup = True misspelled_result = 1 else: user.searchpreference.lookup = False if params.get('fuzzy_search', False): user.searchpreference.trigram.set(SearchFields.objects.all()) misspelled_result = 1 else: user.searchpreference.trigram.set([]) if params.get('icontains', False): user.searchpreference.icontains.set(SearchFields.objects.all()) search_term = 'ghijklmn' else: user.searchpreference.icontains.set([]) if params.get('istartswith', False): user.searchpreference.istartswith.set(SearchFields.objects.all()) search_term = 'abcdef' else: user.searchpreference.istartswith.set([]) if params.get('unaccent', False): user.searchpreference.unaccent.set(SearchFields.objects.all()) misspelled_result *= 2 result *= 2 else: user.searchpreference.unaccent.set([]) # full text vectors are hard coded to use unaccent - put this after unaccent to override result if params.get('fulltext', False): user.searchpreference.fulltext.set(SearchFields.objects.all()) # user.searchpreference.search = 'websearch' search_term = 'ghijklmn uvwxyz' result = 2 else: user.searchpreference.fulltext.set([]) user.searchpreference.save() misspelled_term = transpose(search_term, number=3) return (u1_s1, result, misspelled_result, search_term, misspelled_term, params) @pytest.fixture def recipes(space_1): return RecipeFactory.create_batch(10, space=space_1) @pytest.fixture def found_recipe(request, space_1, accent, unaccent, u1_s1, u2_s1): user1 = auth.get_user(u1_s1) user2 = auth.get_user(u2_s1) days_3 = timezone.now() - timedelta(days=3) days_15 = timezone.now() - timedelta(days=15) days_30 = timezone.now() - timedelta(days=30) if request.param.get('createdon', None): recipe1 = RecipeFactory.create(space=space_1, created_at=days_3) recipe2 = RecipeFactory.create(space=space_1, created_at=days_30) recipe3 = RecipeFactory.create(space=space_1, created_at=days_15) else: recipe1 = RecipeFactory.create(space=space_1) recipe2 = RecipeFactory.create(space=space_1) recipe3 = RecipeFactory.create(space=space_1) obj1 = None obj2 = None if request.param.get('food', None): obj1 = FoodFactory.create(name=unaccent, space=space_1) obj2 = FoodFactory.create(name=accent, space=space_1) recipe1.steps.first().ingredients.add(IngredientFactory.create(food=obj1)) recipe2.steps.first().ingredients.add(IngredientFactory.create(food=obj2)) recipe3.steps.first().ingredients.add(IngredientFactory.create( food=obj1), IngredientFactory.create(food=obj2)) if request.param.get('keyword', None): obj1 = KeywordFactory.create(name=unaccent, space=space_1) obj2 = KeywordFactory.create(name=accent, space=space_1) recipe1.keywords.add(obj1) recipe2.keywords.add(obj2) recipe3.keywords.add(obj1, obj2) recipe1.name = unaccent recipe2.name = accent recipe1.save() recipe2.save() if request.param.get('book', None): obj1 = RecipeBookEntryFactory.create(recipe=recipe1).book obj2 = RecipeBookEntryFactory.create(recipe=recipe2).book RecipeBookEntryFactory.create(recipe=recipe3, book=obj1) RecipeBookEntryFactory.create(recipe=recipe3, book=obj2) if request.param.get('unit', None): obj1 = UnitFactory.create(name=unaccent, space=space_1) obj2 = UnitFactory.create(name=accent, space=space_1) recipe1.steps.first().ingredients.add(IngredientFactory.create(unit=obj1)) recipe2.steps.first().ingredients.add(IngredientFactory.create(unit=obj2)) recipe3.steps.first().ingredients.add(IngredientFactory.create( unit=obj1), IngredientFactory.create(unit=obj2)) if request.param.get('name', None): recipe1.name = unaccent recipe2.name = accent recipe1.save() recipe2.save() if request.param.get('description', None): recipe1.description = unaccent recipe2.description = accent recipe1.save() recipe2.save() if request.param.get('instruction', None): i1 = recipe1.steps.first() i2 = recipe2.steps.first() i1.instruction = unaccent i2.instruction = accent i1.save() i2.save() if request.param.get('viewedon', None): ViewLogFactory.create(recipe=recipe1, created_by=user1, created_at=days_3, space=space_1) ViewLogFactory.create(recipe=recipe2, created_by=user1, created_at=days_30, space=space_1) ViewLogFactory.create(recipe=recipe3, created_by=user2, created_at=days_15, space=space_1) if request.param.get('cookedon', None): CookLogFactory.create(recipe=recipe1, created_by=user1, created_at=days_3, space=space_1) CookLogFactory.create(recipe=recipe2, created_by=user1, created_at=days_30, space=space_1) CookLogFactory.create(recipe=recipe3, created_by=user2, created_at=days_15, space=space_1) if request.param.get('timescooked', None): CookLogFactory.create_batch( 5, recipe=recipe1, created_by=user1, space=space_1) CookLogFactory.create(recipe=recipe2, created_by=user1, space=space_1) CookLogFactory.create_batch( 3, recipe=recipe3, created_by=user2, space=space_1) if request.param.get('rating', None): CookLogFactory.create( recipe=recipe1, created_by=user1, rating=5.0, space=space_1) CookLogFactory.create( recipe=recipe2, created_by=user1, rating=1.0, space=space_1) CookLogFactory.create( recipe=recipe3, created_by=user2, rating=3.0, space=space_1) return (recipe1, recipe2, recipe3, obj1, obj2, request.param) @pytest.mark.parametrize("found_recipe, param_type", [ ({'food': True}, 'foods'), ({'keyword': True}, 'keywords'), ({'book': True}, 'books'), ], indirect=['found_recipe']) @pytest.mark.parametrize('operator', [('_or', 3, 0), ('_and', 1, 2), ]) def test_search_or_and_not(found_recipe, param_type, operator, recipes, u1_s1, space_1): with scope(space=space_1): param1 = f"{param_type}{operator[0]}={found_recipe[3].id}" param2 = f"{param_type}{operator[0]}={found_recipe[4].id}" param1_not = f"{param_type}{operator[0]}_not={found_recipe[3].id}" param2_not = f"{param_type}{operator[0]}_not={found_recipe[4].id}" # testing include searches r = json.loads(u1_s1.get(reverse(LIST_URL) + f'?{param1}').content) assert r['count'] == 2 assert found_recipe[0].id in [x['id'] for x in r['results']] assert found_recipe[2].id in [x['id'] for x in r['results']] r = json.loads(u1_s1.get(reverse(LIST_URL) + f'?{param2}').content) assert r['count'] == 2 assert found_recipe[1].id in [x['id'] for x in r['results']] assert found_recipe[2].id in [x['id'] for x in r['results']] r = json.loads(u1_s1.get(reverse(LIST_URL) + f'?{param1}&{param2}').content) assert r['count'] == operator[1] assert found_recipe[2].id in [x['id'] for x in r['results']] # testing _not searches r = json.loads(u1_s1.get(reverse(LIST_URL) + f'?{param1_not}').content) assert r['count'] == 11 assert found_recipe[0].id not in [x['id'] for x in r['results']] assert found_recipe[2].id not in [x['id'] for x in r['results']] r = json.loads(u1_s1.get(reverse(LIST_URL) + f'?{param2_not}').content) assert r['count'] == 11 assert found_recipe[1].id not in [x['id'] for x in r['results']] assert found_recipe[2].id not in [x['id'] for x in r['results']] r = json.loads(u1_s1.get(reverse(LIST_URL) + f'?{param1_not}&{param2_not}').content) assert r['count'] == 10 + operator[2] assert found_recipe[2].id not in [x['id'] for x in r['results']] @pytest.mark.parametrize("found_recipe", [ ({'unit': True}), ], indirect=['found_recipe']) def test_search_units(found_recipe, recipes, u1_s1, space_1): with scope(space=space_1): param1 = f"units={found_recipe[3].id}" param2 = f"units={found_recipe[4].id}" # testing include searches r = json.loads(u1_s1.get(reverse(LIST_URL) + f'?{param1}').content) assert r['count'] == 2 assert found_recipe[0].id in [x['id'] for x in r['results']] assert found_recipe[2].id in [x['id'] for x in r['results']] r = json.loads(u1_s1.get(reverse(LIST_URL) + f'?{param2}').content) assert r['count'] == 2 assert found_recipe[1].id in [x['id'] for x in r['results']] assert found_recipe[2].id in [x['id'] for x in r['results']] r = json.loads(u1_s1.get(reverse(LIST_URL) + f'?{param1}&{param2}').content) assert r['count'] == 3 assert found_recipe[2].id in [x['id'] for x in r['results']] @pytest.mark.skipif(sqlite, reason="requires PostgreSQL") @pytest.mark.parametrize("user1", itertools.product( [ ('fuzzy_search', True), ('fuzzy_search', False), ('fuzzy_lookups', True), ('fuzzy_lookups', False) ], [('unaccent', True), ('unaccent', False)] ), indirect=['user1']) @pytest.mark.parametrize("found_recipe, param_type", [ ({'unit': True}, 'unit'), ({'keyword': True}, 'keyword'), ({'food': True}, 'food'), ], indirect=['found_recipe']) def test_fuzzy_lookup(found_recipe, recipes, param_type, user1, space_1): with scope(space=space_1): list_url = f'api:{param_type}-list' param1 = f"query={user1[3]}" param2 = f"query={user1[4]}" r = json.loads(user1[0].get(reverse(list_url) + f'?{param1}&limit=2').content) assert len([x['id'] for x in r['results'] if x['id'] in [ found_recipe[3].id, found_recipe[4].id]]) == user1[1] r = json.loads(user1[0].get(reverse(list_url) + f'?{param2}&limit=10').content) assert len([x['id'] for x in r['results'] if x['id'] in [ found_recipe[3].id, found_recipe[4].id]]) == user1[2] # commenting this out for general use - it is really slow # it should be run on occasion to ensure everything still works # @pytest.mark.skipif(sqlite and True, reason="requires PostgreSQL") # @pytest.mark.parametrize("user1", itertools.product( # [ # ('fuzzy_search', True), ('fuzzy_search', False), # ('fulltext', True), ('fulltext', False), # ('icontains', True), ('icontains', False), # ('istartswith', True), ('istartswith', False), # ], # [('unaccent', True), ('unaccent', False)] # ), indirect=['user1']) # @pytest.mark.parametrize("found_recipe", [ # ({'name': True}), # ({'description': True}), # ({'instruction': True}), # ({'keyword': True}), # ({'food': True}), # ], indirect=['found_recipe']) # # user array contains: user client, expected count of search, expected count of mispelled search, search string, mispelled search string, user search preferences # def test_search_string(found_recipe, recipes, user1, space_1): # with scope(space=space_1): # param1 = f"query={user1[3]}" # param2 = f"query={user1[4]}" # r = json.loads(user1[0].get(reverse(LIST_URL) + f'?{param1}').content) # assert len([x['id'] for x in r['results'] if x['id'] in [ # found_recipe[0].id, found_recipe[1].id]]) == user1[1] # r = json.loads(user1[0].get(reverse(LIST_URL) + f'?{param2}').content) # assert len([x['id'] for x in r['results'] if x['id'] in [ # found_recipe[0].id, found_recipe[1].id]]) == user1[2] @pytest.mark.parametrize("found_recipe, param_type, result", [ ({'viewedon': True}, 'viewedon', (1, 1)), ({'cookedon': True}, 'cookedon', (1, 1)), # created dates are not filtered by user ({'createdon': True}, 'createdon', (2, 12)), # updated dates are not filtered by user ({'createdon': True}, 'updatedon', (2, 12)), ], indirect=['found_recipe']) def test_search_date(found_recipe, recipes, param_type, result, u1_s1, u2_s1, space_1): # force updated_at to equal created_at datetime with scope(space=space_1): for recipe in Recipe.objects.all(): Recipe.objects.filter(id=recipe.id).update( updated_at=recipe.created_at) date = (timezone.now() - timedelta(days=15)).strftime("%Y-%m-%d") param1 = f"?{param_type}={date}" param2 = f"?{param_type}=-{date}" r = json.loads(u1_s1.get(reverse(LIST_URL) + f'{param1}').content) assert r['count'] == result[0] assert found_recipe[0].id in [x['id'] for x in r['results']] r = json.loads(u1_s1.get(reverse(LIST_URL) + f'{param2}').content) assert r['count'] == result[1] assert found_recipe[1].id in [x['id'] for x in r['results']] # test today's date returns for lte and gte searches r = json.loads(u2_s1.get(reverse(LIST_URL) + f'{param1}').content) assert r['count'] == result[0] assert found_recipe[2].id in [x['id'] for x in r['results']] r = json.loads(u2_s1.get(reverse(LIST_URL) + f'{param2}').content) assert r['count'] == result[1] assert found_recipe[2].id in [x['id'] for x in r['results']] @pytest.mark.parametrize("found_recipe, param_type", [ ({'rating': True}, 'rating'), ({'timescooked': True}, 'timescooked'), ], indirect=['found_recipe']) def test_search_count(found_recipe, recipes, param_type, u1_s1, u2_s1, space_1): param1 = f'?{param_type}=3' param2 = f'?{param_type}=-3' param3 = f'?{param_type}=0' r = json.loads(u1_s1.get(reverse(LIST_URL) + param1).content) assert r['count'] == 1 assert found_recipe[0].id in [x['id'] for x in r['results']] r = json.loads(u1_s1.get(reverse(LIST_URL) + param2).content) assert r['count'] == 1 assert found_recipe[1].id in [x['id'] for x in r['results']] # test search for not rated/cooked r = json.loads(u1_s1.get(reverse(LIST_URL) + param3).content) assert r['count'] == 11 assert (found_recipe[0].id or found_recipe[1].id) not in [ x['id'] for x in r['results']] # test matched returns for lte and gte searches r = json.loads(u2_s1.get(reverse(LIST_URL) + param1).content) assert r['count'] == 1 assert found_recipe[2].id in [x['id'] for x in r['results']] r = json.loads(u2_s1.get(reverse(LIST_URL) + param2).content) assert r['count'] == 1 assert found_recipe[2].id in [x['id'] for x in r['results']]