""" Test for recipe APIs. """ import tempfile import os from PIL import Image from decimal import Decimal from django.contrib.auth import get_user_model from django.test import TestCase from django. urls import reverse from rest_framework import status from rest_framework.test import APIClient from core.models import ( Recipe, Tag, Ingredient, ) from recipe.serializers import ( RecipeSerializer, RecipeDetailSerializer, ) RECIPES_URL = reverse('recipe:recipe-list') def detail_url(recipe_id): """Create and return a recipe detail URL.""" return reverse('recipe:recipe-detail', args=[recipe_id]) def image_upload_url(recipe_id): """Create and return an image upload URL.""" return reverse('recipe:recipe-upload-image', args=[recipe_id]) def create_recipe(user, **params): """Create and return a sample recipe.""" defaults = { 'title': 'Titulo receta de ejemplo', 'time_minutes': 31, 'price': Decimal('5.25'), 'description': 'Descripción de ejemplo', 'link': 'https://defzn.kickto.net/blog', } defaults.update(params) recipe = Recipe.objects.create(user=user, **defaults) return recipe def create_user(**params): """Create and create a new user.""" return get_user_model().objects.create_user(**params) class PublicRecipeApiTests(TestCase): """Test unauthenticated API requests.""" def setUp(self): self.client = APIClient() def test_auth_required(self): """Test auth is required to call API.""" res = self.client.get(RECIPES_URL) self.assertEqual(res.status_code, status.HTTP_401_UNAUTHORIZED) class PrivateRecipeApiTests(TestCase): """Test authenticated API requests.""" def setUp(self): self.client = APIClient() self.user = create_user( email='user@example.com', password='testpass123' ) self.client.force_authenticate(self.user) def test_retrive_recipes(self): """Test retrieving a list of recipes.""" create_recipe(user=self.user) create_recipe(user=self.user) res = self.client.get(RECIPES_URL) recipes = Recipe.objects.all().order_by('-id') serializer = RecipeSerializer(recipes, many=True) self.assertEqual(res.status_code, status.HTTP_200_OK) self.assertEqual(res.data, serializer.data) def test_recipe_list_limited_to_user(self): """Test list of recipes is limited to authenticated user.""" other_user = create_user( email='other@example.com', password='password123' ) create_recipe(user=other_user) create_recipe(user=self.user) res = self.client.get(RECIPES_URL) recipes = Recipe.objects.filter(user=self.user) serializer = RecipeSerializer(recipes, many=True) self.assertEqual(res.status_code, status.HTTP_200_OK) self.assertEqual(res.data, serializer.data) def test_get_recipe_detail(self): """Test get recipe detail.""" recipe = create_recipe(user=self.user) url = detail_url(recipe.id) res = self.client.get(url) serializer = RecipeDetailSerializer(recipe) self.assertEqual(res.data, serializer.data) def test_create_recipe(self): """Test creating a recipe.""" payload = { 'title': 'Titulo receta de ejemplo', 'time_minutes': 16, 'price': Decimal('5.99'), } res = self.client.post(RECIPES_URL, payload) self.assertEqual(res.status_code, status.HTTP_201_CREATED) recipe = Recipe.objects.get(id=res.data['id']) for k, v in payload.items(): self.assertEqual(getattr(recipe, k), v) self.assertEqual(recipe.user, self.user) def test_partial_update(self): """Test partial update of a recipe.""" original_link = 'https://devfzn.kickto.net/acerca' recipe = create_recipe( user=self.user, title='Titulo de la Receta de ejemplo', link=original_link, ) payload = {'title': 'Nuevo titulo de la receta de ejemplo'} url = detail_url(recipe.id) res = self.client.patch(url, payload) self.assertEqual(res.status_code, status.HTTP_200_OK) recipe.refresh_from_db() self.assertEqual(recipe.title, payload['title']) self.assertEqual(recipe.link, original_link) self.assertEqual(recipe.user, self.user) def test_full_update(self): """Test full update of recipe.""" recipe = create_recipe( user=self.user, title='Titulo receta de ejemplo', link='https://devfzn.kickto.net/blog', description='Descripción receta de ejemplo', ) payload = { 'title': 'Titulo receta de ejemplo', 'link': 'https://defzn.kickto.net/blog', 'description': 'Descripción de ejemplo', 'time_minutes': 10, 'price': Decimal('3.65'), } url = detail_url(recipe.id) res = self.client.put(url, payload) self.assertEqual(res.status_code, status.HTTP_200_OK) recipe.refresh_from_db() for k, v in payload.items(): self.assertEqual(getattr(recipe, k), v) self.assertEqual(recipe.user, self.user) def test_update_user_returns_error(self): """Test changing the recipe user results in an error.""" new_user = create_user( email='user2@example.com', password='testpass123' ) recipe = create_recipe(user=self.user) payload = {'user': new_user.id} url = detail_url(recipe.id) self.client.patch(url, payload) recipe.refresh_from_db() self.assertEqual(recipe.user, self.user) def test_delete_recipe(self): """Test deleting a recipe sucessful.""" recipe = create_recipe(user=self.user) url = detail_url(recipe.id) res = self.client.delete(url) self.assertEqual(res.status_code, status.HTTP_204_NO_CONTENT) self.assertFalse(Recipe.objects.filter(id=recipe.id).exists()) def test_recipe_other_users_recipe_error(self): """Test trying to delete another users recipe gives error.""" new_user = create_user( email='user2@example.com', password='testpass123' ) recipe = create_recipe(user=new_user) url = detail_url(recipe.id) res = self.client.delete(url) self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND) self.assertTrue(Recipe.objects.filter(id=recipe.id).exists()) def test_create_recipe_with_new_tags(self): """Test create a recipe with new tags.""" payload = { 'title': 'Titulo receta de ejemplo tag anidado', 'time_minutes': 1, 'price': Decimal('2.50'), 'tags': [{'name': 'anidado'}, {'name': 'cena'}] } res = self.client.post(RECIPES_URL, payload, format='json') self.assertEqual(res.status_code, status.HTTP_201_CREATED) recipes = Recipe.objects.filter(user=self.user) self.assertEqual(recipes.count(), 1) recipe = recipes[0] self.assertEqual(recipe.tags.count(), 2) for tag in payload['tags']: exists = recipe.tags.filter( name=tag['name'], user=self.user, ).exists() self.assertTrue(exists) def test_create_recipe_with_existing_tags(self): """Test creating a recipe with existing tag.""" tag_peru = Tag.objects.create(user=self.user, name='Peruana') payload = { 'title': 'Arroz con mariscos', 'time_minutes': 45, 'price': Decimal('8.50'), 'tags': [{'name': 'Peruana'}, {'name': 'Almuerzo'}] } res = self.client.post(RECIPES_URL, payload, format='json') self.assertEqual(res.status_code, status.HTTP_201_CREATED) recipes = Recipe.objects.filter(user=self.user) self.assertEqual(recipes.count(), 1) recipe = recipes[0] self.assertEqual(recipe.tags.count(), 2) self.assertIn(tag_peru, recipe.tags.all()) for tag in payload['tags']: exists = recipe.tags.filter( name=tag['name'], user=self.user, ) self.assertTrue(exists) def test_create_tag_on_update(self): """Test creating tag when updating a recipe.""" recipe = create_recipe(user=self.user) payload = {'tags': [{'name': 'Cena'}]} url = detail_url(recipe.id) res = self.client.patch(url, payload, format='json') self.assertEqual(res.status_code, status.HTTP_200_OK) new_tag = Tag.objects.get(user=self.user, name='Cena') self.assertIn(new_tag, recipe.tags.all()) def test_update_recipe_assign_tag(self): """Test assigning an existing tag when updating a recipe.""" tag_breakfast = Tag.objects.create(user=self.user, name='Desayuno') recipe = create_recipe(user=self.user) recipe.tags.add(tag_breakfast) tag_lunch = Tag.objects.create(user=self.user, name='Cena') payload = {'tags': [{'name': 'Cena'}]} url = detail_url(recipe.id) res = self.client.patch(url, payload, format='json') self.assertEqual(res.status_code, status.HTTP_200_OK) self.assertIn(tag_lunch, recipe.tags.all()) self.assertNotIn(tag_breakfast, recipe.tags.all()) def test_clear_recipe_tags(self): """Test clearing a recipes tags.""" tag = Tag.objects.create(user=self.user, name='Once') recipe = create_recipe(user=self.user) recipe.tags.add(tag) payload = {'tags': []} url = detail_url(recipe.id) res = self.client.patch(url, payload, format='json') self.assertEqual(res.status_code, status.HTTP_200_OK) self.assertEqual(recipe.tags.count(), 0) def test_create_recipe_with_new_ingredients(self): """Test creating a recipe with new ingredients.""" payload = { 'title': 'Camarones acaramelados', 'time_minutes': 45, 'price': Decimal('75.50'), 'ingredients': [{'name': 'Camarones'}, {'name': 'Azucar'}], } res = self.client.post(RECIPES_URL, payload, format='json') self.assertEqual(res.status_code, status.HTTP_201_CREATED) recipes = Recipe.objects.filter(user=self.user) self.assertEqual(recipes.count(), 1) recipe = recipes[0] self.assertEqual(recipe.ingredients.count(), 2) for ingredient in payload['ingredients']: exists = recipe.ingredients.filter( name=ingredient['name'], user=self.user, ).exists() self.assertTrue(exists) def test_create_recipe_with_existing_ingredient(self): """Test creating a new recipe with existing ingredient.""" ingredient = Ingredient.objects.create(user=self.user, name='Limón') payload = { 'title': 'Limonada', 'time_minutes': 15, 'price': '2.50', 'ingredients': [{'name': 'Limón'}, {'name': 'Azucar'}], } res = self.client.post(RECIPES_URL, payload, format='json') self.assertEqual(res.status_code, status.HTTP_201_CREATED) recipes = Recipe.objects.filter(user=self.user) self.assertEqual(recipes.count(), 1) recipe = recipes[0] self.assertEqual(recipe.ingredients.count(), 2) self.assertIn(ingredient, recipe.ingredients.all()) for ingredient in payload['ingredients']: exists = recipe.ingredients.filter( name=ingredient['name'], user=self.user, ).exists() self.assertTrue(exists) def test_create_ingredient_on_update(self): """Test creating an ingredient when updating a recipe.""" recipe = create_recipe(user=self.user) payload = {'ingredients': [{'name': 'Pomelo'}]} url = detail_url(recipe.id) res = self.client.patch(url, payload, format='json') self.assertEqual(res.status_code, status.HTTP_200_OK) new_ingredient = Ingredient.objects.get(user=self.user, name='Pomelo') self.assertIn(new_ingredient, recipe.ingredients.all()) def test_update_recipe_assign_ingredient(self): """Test assigning an existing ingredient when updating a recipe.""" ingredient1 = Ingredient.objects.create( user=self.user, name='Pimienta' ) recipe = create_recipe(user=self.user) recipe.ingredients.add(ingredient1) ingredient2 = Ingredient.objects.create(user=self.user, name='Ají') payload = {'ingredients': [{'name': 'Ají'}]} url = detail_url(recipe.id) res = self.client.patch(url, payload, format='json') self.assertEqual(res.status_code, status.HTTP_200_OK) self.assertIn(ingredient2, recipe.ingredients.all()) self.assertNotIn(ingredient1, recipe.ingredients.all()) def test_clear_recipe_ingredients(self): """Test clearing a recipes ingredients.""" ingredient = Ingredient.objects.create(user=self.user, name='Ajo') recipe = create_recipe(user=self.user) recipe.ingredients.add(ingredient) payload = {'ingredients': []} url = detail_url(recipe.id) res = self.client.patch(url, payload, format='json') self.assertEqual(res.status_code, status.HTTP_200_OK) self.assertEqual(recipe.ingredients.count(), 0) def test_filter_by_tags(self): """Test filtering recipes by tags.""" r1 = create_recipe(user=self.user, title='Sopa de Verduras') r2 = create_recipe(user=self.user, title='Arroz con Huevo') tag1 = Tag.objects.create(user=self.user, name='Vergan') tag2 = Tag.objects.create(user=self.user, name='Vegetariana') r1.tags.add(tag1) r2.tags.add(tag2) r3 = create_recipe(user=self.user, title='Pure con Prietas') params = {'tags': f'{tag1.id}, {tag2.id}'} res = self.client.get(RECIPES_URL, params) s1 = RecipeSerializer(r1) s2 = RecipeSerializer(r2) s3 = RecipeSerializer(r3) self.assertIn(s1.data, res.data) self.assertIn(s2.data, res.data) self.assertNotIn(s3.data, res.data) def test_filter_by_ingredients(self): """Test filtering recipes by ingredients.""" r1 = create_recipe(user=self.user, title='Porotos con rienda') r2 = create_recipe(user=self.user, title='Pollo al jugo') in1 = Ingredient.objects.create(user=self.user, name='Porotos') in2 = Ingredient.objects.create(user=self.user, name='Pollo') r1.ingredients.add(in1) r2.ingredients.add(in2) r3 = create_recipe(user=self.user, title='Lentejas con arroz') params = {'ingredients': f'{in1.id}, {in2.id}'} res = self.client.get(RECIPES_URL, params) s1 = RecipeSerializer(r1) s2 = RecipeSerializer(r2) s3 = RecipeSerializer(r3) self.assertIn(s1.data, res.data) self.assertIn(s2.data, res.data) self.assertNotIn(s3.data, res.data) class ImageUploadTest(TestCase): """Tests for the image upload API.""" def setUp(self): self.client = APIClient() self.user = get_user_model().objects.create_user( 'user@example.com', 'password123', ) self.client.force_authenticate(self.user) self.recipe = create_recipe(user=self.user) def tearDown(self): self.recipe.image.delete() def test_upload_image(self): """Test uploading an image to a recipe.""" url = image_upload_url(self.recipe.id) with tempfile.NamedTemporaryFile(suffix='.jpg') as image_file: img = Image.new('RGB', (10, 10)) img.save(image_file, format='JPEG') image_file.seek(0) payload = {'image': image_file} res = self.client.post(url, payload, format='multipart') self.recipe.refresh_from_db() self.assertEqual(res.status_code, status.HTTP_200_OK) self.assertIn('image', res.data) self.assertTrue(os.path.exists(self.recipe.image.path)) def test_upload_image_bad_request(self): """Test uploading invalid image.""" url = image_upload_url(self.recipe.id) payload = {'image': 'notanimage'} res = self.client.post(url, payload, format='multipart') self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST)