Improve Test Coverage (#511)

* add recipe scaling notes

* test theme rename

* fix coverage call to use poetry

* remove print

* remove async

* consolidate test case data

* fix mealplan tests

* remove redundant else

Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
Hayden 2021-06-13 13:09:44 -08:00 committed by GitHub
parent c325a49fc2
commit 2dc9c8e843
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 249 additions and 230 deletions

View File

@ -116,6 +116,7 @@
- All images are now converted to .webp for better compression
### Behind the Scenes
- The database layer has been added for future recipe scaling.
- Black and Flake8 now run as CI/CD checks
- New debian based docker image
- Unified Sidebar Components

View File

@ -56,8 +56,8 @@ lint: ## 🧺 Check style with flake8
coverage: ## ☂️ Check code coverage quickly with the default Python
poetry run pytest
coverage report -m
coverage html
poetry run coverage report -m
poetry run coverage html
$(BROWSER) htmlcov/index.html
setup: ## 🏗 Setup Development Instance

View File

@ -34,7 +34,6 @@ def authenticate_user(session, email: str, password: str) -> UserInDB:
if not user:
return False
print(user)
if not verify_password(password, user.password):
return False
return user

View File

@ -19,8 +19,7 @@ from mealie.schema.comments import CommentOut
from mealie.schema.event_notifications import EventNotificationIn
from mealie.schema.events import Event as EventSchema
from mealie.schema.meal import MealPlanOut
from mealie.schema.recipe import (Recipe, RecipeIngredientFood,
RecipeIngredientUnit)
from mealie.schema.recipe import Recipe, RecipeIngredientFood, RecipeIngredientUnit
from mealie.schema.settings import CustomPageOut
from mealie.schema.settings import SiteSettings as SiteSettingsSchema
from mealie.schema.shopping_list import ShoppingListOut

View File

@ -85,7 +85,7 @@ def validate_long_live_token(session: Session, client_token: str, id: int) -> Us
return token.user
async def validate_file_token(token: Optional[str] = None) -> Path:
def validate_file_token(token: Optional[str] = None) -> Path:
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="could not validate file token",

View File

@ -38,14 +38,14 @@ def clean(recipe_data: dict, url=None) -> dict:
def clean_string(text: str) -> str:
if text == "" or text is None:
return ""
else:
cleaned_text = html.unescape(text)
cleaned_text = re.sub("<[^<]+?>", "", cleaned_text)
cleaned_text = re.sub(" +", " ", cleaned_text)
cleaned_text = re.sub("</p>", "\n", cleaned_text)
cleaned_text = re.sub(r"\n\s*\n", "\n\n", cleaned_text)
cleaned_text = cleaned_text.replace("\xa0", " ").replace("\t", " ").strip()
return cleaned_text
cleaned_text = html.unescape(text)
cleaned_text = re.sub("<[^<]+?>", "", cleaned_text)
cleaned_text = re.sub(" +", " ", cleaned_text)
cleaned_text = re.sub("</p>", "\n", cleaned_text)
cleaned_text = re.sub(r"\n\s*\n", "\n\n", cleaned_text)
cleaned_text = cleaned_text.replace("\xa0", " ").replace("\t", " ").strip()
return cleaned_text
def category(category: str):
@ -82,6 +82,10 @@ def instructions(instructions) -> List[dict]:
if not instructions:
return []
# Dictionary (Keys: step number strings, Values: the instructions)
if isinstance(instructions, dict):
instructions = list(instructions.values())
if isinstance(instructions, list) and isinstance(instructions[0], list):
instructions = instructions[0]

View File

@ -11,7 +11,7 @@ from pytest import fixture
from tests.app_routes import AppRoutes
from tests.test_config import TEST_DATA
from tests.utils.recipe_data import build_recipe_store, get_raw_no_image, get_raw_recipe
from tests.utils.recipe_data import get_raw_no_image, get_raw_recipe, get_recipe_test_cases
main()
@ -71,4 +71,4 @@ def raw_recipe_no_image():
@fixture(scope="session")
def recipe_store():
return build_recipe_store()
return get_recipe_test_cases()

View File

@ -4,16 +4,17 @@ import pytest
from fastapi.testclient import TestClient
from slugify import slugify
from tests.app_routes import AppRoutes
from tests.utils.recipe_data import RecipeTestData, build_recipe_store
from tests.utils.recipe_data import RecipeSiteTestCase, get_recipe_test_cases
recipe_test_data = build_recipe_store()
recipe_test_data = get_recipe_test_cases()
@pytest.mark.parametrize("recipe_data", recipe_test_data)
def test_create_by_url(api_client: TestClient, api_routes: AppRoutes, recipe_data: RecipeTestData, token):
def test_create_by_url(api_client: TestClient, api_routes: AppRoutes, recipe_data: RecipeSiteTestCase, token):
api_client.delete(api_routes.recipes_recipe_slug(recipe_data.expected_slug), headers=token)
response = api_client.post(api_routes.recipes_create_url, json={"url": recipe_data.url}, headers=token)
assert response.status_code == 201
assert json.loads(response.text) == recipe_data.expected_slug
@ -35,7 +36,7 @@ def test_create_no_image(api_client: TestClient, api_routes: AppRoutes, token, r
@pytest.mark.parametrize("recipe_data", recipe_test_data)
def test_read_update(api_client: TestClient, api_routes: AppRoutes, recipe_data: RecipeTestData, token):
def test_read_update(api_client: TestClient, api_routes: AppRoutes, recipe_data: RecipeSiteTestCase, token):
recipe_url = api_routes.recipes_recipe_slug(recipe_data.expected_slug)
response = api_client.get(recipe_url, headers=token)
assert response.status_code == 200
@ -68,7 +69,7 @@ def test_read_update(api_client: TestClient, api_routes: AppRoutes, recipe_data:
@pytest.mark.parametrize("recipe_data", recipe_test_data)
def test_rename(api_client: TestClient, api_routes: AppRoutes, recipe_data: RecipeTestData, token):
def test_rename(api_client: TestClient, api_routes: AppRoutes, recipe_data: RecipeSiteTestCase, token):
recipe_url = api_routes.recipes_recipe_slug(recipe_data.expected_slug)
response = api_client.get(recipe_url, headers=token)
assert response.status_code == 200
@ -87,7 +88,7 @@ def test_rename(api_client: TestClient, api_routes: AppRoutes, recipe_data: Reci
@pytest.mark.parametrize("recipe_data", recipe_test_data)
def test_delete(api_client: TestClient, api_routes: AppRoutes, recipe_data: RecipeTestData, token):
def test_delete(api_client: TestClient, api_routes: AppRoutes, recipe_data: RecipeSiteTestCase, token):
recipe_url = api_routes.recipes_recipe_slug(recipe_data.expected_slug)
response = api_client.delete(recipe_url, headers=token)
assert response.status_code == 200

View File

@ -1,104 +1,102 @@
# import json
import json
# import pytest
# from fastapi.testclient import TestClient
# from tests.app_routes import AppRoutes
# from tests.utils.recipe_data import RecipeTestData
import pytest
from fastapi.testclient import TestClient
from tests.app_routes import AppRoutes
from tests.utils.recipe_data import RecipeSiteTestCase
# def get_meal_plan_template(first=None, second=None):
# return {
# "group": "Home",
# "startDate": "2021-01-18",
# "endDate": "2021-01-19",
# "meals": [
# {
# "slug": first,
# "date": "2021-1-17",
# },
# {
# "slug": second,
# "date": "2021-1-18",
# },
# ],
# }
def get_meal_plan_template(first=None, second=None):
return {
"group": "Home",
"startDate": "2021-01-18",
"endDate": "2021-01-19",
"planDays": [
{
"date": "2021-1-18",
"meals": [{"slug": first, "name": "", "description": ""}],
},
{
"date": "2021-1-19",
"meals": [{"slug": second, "name": "", "description": ""}],
},
],
}
# @pytest.fixture(scope="session")
# def slug_1(api_client: TestClient, api_routes: AppRoutes, token, recipe_store: list[RecipeTestData]):
# # Slug 1
# slug_1 = api_client.post(api_routes.recipes_create_url, json={"url": recipe_store[0].url}, headers=token)
# slug_1 = json.loads(slug_1.content)
@pytest.fixture(scope="session")
def slug_1(api_client: TestClient, api_routes: AppRoutes, token, recipe_store: list[RecipeSiteTestCase]):
slug_1 = api_client.post(api_routes.recipes_create_url, json={"url": recipe_store[0].url}, headers=token)
slug_1 = json.loads(slug_1.content)
# yield slug_1
yield slug_1
# api_client.delete(api_routes.recipes_recipe_slug(slug_1))
api_client.delete(api_routes.recipes_recipe_slug(slug_1))
# @pytest.fixture(scope="session")
# def slug_2(api_client: TestClient, api_routes: AppRoutes, token, recipe_store: list[RecipeTestData]):
# # Slug 2
# slug_2 = api_client.post(api_routes.recipes_create_url, json={"url": recipe_store[1].url}, headers=token)
# slug_2 = json.loads(slug_2.content)
@pytest.fixture(scope="session")
def slug_2(api_client: TestClient, api_routes: AppRoutes, token, recipe_store: list[RecipeSiteTestCase]):
slug_2 = api_client.post(api_routes.recipes_create_url, json={"url": recipe_store[1].url}, headers=token)
slug_2 = json.loads(slug_2.content)
# yield slug_2
yield slug_2
# api_client.delete(api_routes.recipes_recipe_slug(slug_2))
api_client.delete(api_routes.recipes_recipe_slug(slug_2))
# def test_create_mealplan(api_client: TestClient, api_routes: AppRoutes, slug_1, slug_2, token):
# meal_plan = get_meal_plan_template(slug_1, slug_2)
def test_create_mealplan(api_client: TestClient, api_routes: AppRoutes, slug_1, slug_2, token):
meal_plan = get_meal_plan_template(slug_1, slug_2)
# response = api_client.post(api_routes.meal_plans_create, json=meal_plan, headers=token)
# assert response.status_code == 201
response = api_client.post(api_routes.meal_plans_create, json=meal_plan, headers=token)
assert response.status_code == 201
# def test_read_mealplan(api_client: TestClient, api_routes: AppRoutes, slug_1, slug_2, token):
# response = api_client.get(api_routes.meal_plans_all, headers=token)
def test_read_mealplan(api_client: TestClient, api_routes: AppRoutes, slug_1, slug_2, token):
response = api_client.get(api_routes.meal_plans_all, headers=token)
# assert response.status_code == 200
assert response.status_code == 200
# meal_plan = get_meal_plan_template(slug_1, slug_2)
meal_plan_template = get_meal_plan_template(slug_1, slug_2)
# new_meal_plan = json.loads(response.text)
# meals = new_meal_plan[0]["meals"]
created_meal_plan = json.loads(response.text)
meals = created_meal_plan[0]["planDays"]
# assert meals[0]["slug"] == meal_plan["meals"][0]["slug"]
# assert meals[1]["slug"] == meal_plan["meals"][1]["slug"]
assert meals[0]["meals"][0]["slug"] == meal_plan_template["planDays"][0]["meals"][0]["slug"]
assert meals[1]["meals"][0]["slug"] == meal_plan_template["planDays"][1]["meals"][0]["slug"]
# def test_update_mealplan(api_client: TestClient, api_routes: AppRoutes, slug_1, slug_2, token):
def test_update_mealplan(api_client: TestClient, api_routes: AppRoutes, slug_1, slug_2, token):
# response = api_client.get(api_routes.meal_plans_all, headers=token)
response = api_client.get(api_routes.meal_plans_all, headers=token)
# existing_mealplan = json.loads(response.text)
# existing_mealplan = existing_mealplan[0]
existing_mealplan = json.loads(response.text)
existing_mealplan = existing_mealplan[0]
# # Swap
# plan_uid = existing_mealplan.get("uid")
# existing_mealplan["meals"][0]["slug"] = slug_2
# existing_mealplan["meals"][1]["slug"] = slug_1
# Swap
plan_uid = existing_mealplan.get("uid")
existing_mealplan["planDays"][0]["meals"][0]["slug"] = slug_2
existing_mealplan["planDays"][1]["meals"][0]["slug"] = slug_1
# response = api_client.put(api_routes.meal_plans_plan_id(plan_uid), json=existing_mealplan, headers=token)
response = api_client.put(api_routes.meal_plans_plan_id(plan_uid), json=existing_mealplan, headers=token)
# assert response.status_code == 200
assert response.status_code == 200
# response = api_client.get(api_routes.meal_plans_all, headers=token)
# existing_mealplan = json.loads(response.text)
# existing_mealplan = existing_mealplan[0]
response = api_client.get(api_routes.meal_plans_all, headers=token)
existing_mealplan = json.loads(response.text)
existing_mealplan = existing_mealplan[0]
# assert existing_mealplan["meals"][0]["slug"] == slug_2
# assert existing_mealplan["meals"][1]["slug"] == slug_1
assert existing_mealplan["planDays"][0]["meals"][0]["slug"] == slug_2
assert existing_mealplan["planDays"][1]["meals"][0]["slug"] == slug_1
# def test_delete_mealplan(api_client: TestClient, api_routes: AppRoutes, token):
# response = api_client.get(api_routes.meal_plans_all, headers=token)
def test_delete_mealplan(api_client: TestClient, api_routes: AppRoutes, token):
response = api_client.get(api_routes.meal_plans_all, headers=token)
# assert response.status_code == 200
# existing_mealplan = json.loads(response.text)
# existing_mealplan = existing_mealplan[0]
assert response.status_code == 200
existing_mealplan = json.loads(response.text)
existing_mealplan = existing_mealplan[0]
# plan_uid = existing_mealplan.get("uid")
# response = api_client.delete(api_routes.meal_plans_plan_id(plan_uid), headers=token)
plan_uid = existing_mealplan.get("uid")
response = api_client.delete(api_routes.meal_plans_plan_id(plan_uid), headers=token)
# assert response.status_code == 200
assert response.status_code == 200

View File

@ -3,7 +3,6 @@ import json
import pytest
from fastapi.testclient import TestClient
from mealie.schema.settings import SiteSettings
from mealie.schema.theme import SiteTheme
from tests.app_routes import AppRoutes
@ -12,28 +11,6 @@ def default_settings():
return SiteSettings().dict(by_alias=True)
@pytest.fixture(scope="session")
def default_theme():
return SiteTheme(id=1).dict()
@pytest.fixture(scope="session")
def new_theme():
return {
"id": 3,
"name": "myTestTheme",
"colors": {
"primary": "#E58325",
"accent": "#00457A",
"secondary": "#973542",
"success": "#43A047",
"info": "#4990BA",
"warning": "#FF4081",
"error": "#EF5350",
},
}
def test_default_settings(api_client: TestClient, api_routes: AppRoutes, default_settings):
response = api_client.get(api_routes.site_settings)
@ -52,59 +29,3 @@ def test_update_settings(api_client: TestClient, api_routes: AppRoutes, default_
response = api_client.get(api_routes.site_settings)
assert json.loads(response.content) == default_settings
def test_default_theme(api_client: TestClient, api_routes: AppRoutes, default_theme):
response = api_client.get(api_routes.themes_id(1))
assert response.status_code == 200
assert json.loads(response.content) == default_theme
def test_create_theme(api_client: TestClient, api_routes: AppRoutes, new_theme, token):
response = api_client.post(api_routes.themes_create, json=new_theme, headers=token)
assert response.status_code == 201
response = api_client.get(api_routes.themes_id(new_theme.get("id")), headers=token)
assert response.status_code == 200
assert json.loads(response.content) == new_theme
def test_read_all_themes(api_client: TestClient, api_routes: AppRoutes, default_theme, new_theme):
response = api_client.get(api_routes.themes)
assert response.status_code == 200
response_dict = json.loads(response.content)
assert default_theme in response_dict
assert new_theme in response_dict
def test_read_theme(api_client: TestClient, api_routes: AppRoutes, default_theme, new_theme):
for theme in [default_theme, new_theme]:
response = api_client.get(api_routes.themes_id(theme.get("id")))
assert response.status_code == 200
assert json.loads(response.content) == theme
def test_update_theme(api_client: TestClient, api_routes: AppRoutes, token, default_theme, new_theme):
theme_colors = {
"primary": "#E12345",
"accent": "#012345",
"secondary": "#973542",
"success": "#5AB1BB",
"info": "#4990BA",
"warning": "#FF4081",
"error": "#EF4432",
}
new_theme["colors"] = theme_colors
response = api_client.put(api_routes.themes_id(new_theme.get("id")), json=new_theme, headers=token)
assert response.status_code == 200
response = api_client.get(api_routes.themes_id(new_theme.get("id")))
assert json.loads(response.content) == new_theme
def test_delete_theme(api_client: TestClient, api_routes: AppRoutes, default_theme, new_theme, token):
for theme in [default_theme, new_theme]:
response = api_client.delete(api_routes.themes_id(theme.get("id")), headers=token)
assert response.status_code == 200

View File

@ -0,0 +1,85 @@
import json
import pytest
from fastapi.testclient import TestClient
from mealie.schema.theme import SiteTheme
from tests.app_routes import AppRoutes
@pytest.fixture(scope="session")
def default_theme():
return SiteTheme(id=1).dict()
@pytest.fixture(scope="session")
def new_theme():
return {
"id": 3,
"name": "myTestTheme",
"colors": {
"primary": "#E58325",
"accent": "#00457A",
"secondary": "#973542",
"success": "#43A047",
"info": "#4990BA",
"warning": "#FF4081",
"error": "#EF5350",
},
}
def test_default_theme(api_client: TestClient, api_routes: AppRoutes, default_theme):
response = api_client.get(api_routes.themes_id(1))
assert response.status_code == 200
assert json.loads(response.content) == default_theme
def test_create_theme(api_client: TestClient, api_routes: AppRoutes, new_theme, token):
response = api_client.post(api_routes.themes_create, json=new_theme, headers=token)
assert response.status_code == 201
response = api_client.get(api_routes.themes_id(new_theme.get("id")), headers=token)
assert response.status_code == 200
assert json.loads(response.content) == new_theme
def test_read_all_themes(api_client: TestClient, api_routes: AppRoutes, default_theme, new_theme):
response = api_client.get(api_routes.themes)
assert response.status_code == 200
response_dict = json.loads(response.content)
assert default_theme in response_dict
assert new_theme in response_dict
def test_read_theme(api_client: TestClient, api_routes: AppRoutes, default_theme, new_theme):
for theme in [default_theme, new_theme]:
response = api_client.get(api_routes.themes_id(theme.get("id")))
assert response.status_code == 200
assert json.loads(response.content) == theme
def test_update_theme(api_client: TestClient, api_routes: AppRoutes, token, new_theme):
theme_colors = {
"primary": "#E12345",
"accent": "#012345",
"secondary": "#973542",
"success": "#5AB1BB",
"info": "#4990BA",
"warning": "#FF4081",
"error": "#EF4432",
}
new_theme["colors"] = theme_colors
new_theme["name"] = "New Theme Name"
response = api_client.put(api_routes.themes_id(new_theme.get("id")), json=new_theme, headers=token)
assert response.status_code == 200
response = api_client.get(api_routes.themes_id(new_theme.get("id")))
assert json.loads(response.content) == new_theme
def test_delete_theme(api_client: TestClient, api_routes: AppRoutes, default_theme, new_theme, token):
for theme in [default_theme, new_theme]:
response = api_client.delete(api_routes.themes_id(theme.get("id")), headers=token)
assert response.status_code == 200

View File

@ -34,6 +34,13 @@ def new_user():
)
def test_failed_login(api_client: TestClient, api_routes: AppRoutes):
form_data = {"username": "changeme@email.com", "password": "WRONG_PASSWORD"}
response = api_client.post(api_routes.auth_token, form_data)
assert response.status_code == 401
def test_superuser_login(api_client: TestClient, api_routes: AppRoutes, token):
form_data = {"username": "changeme@email.com", "password": "MyPassword"}
response = api_client.post(api_routes.auth_token, form_data)

View File

@ -1,57 +1,17 @@
from dataclasses import dataclass
import pytest
from mealie.services.scraper import scraper
from tests.utils.recipe_data import RecipeSiteTestCase, get_recipe_test_cases
test_cases = get_recipe_test_cases()
"""
These tests are skipped by default and only really used when troubleshooting the parser
directly. If you are working on improve the parser you can add test cases to the `get_recipe_test_cases` function
and then use this test case by removing the `@pytest.mark.skip` and than testing your results.
"""
@dataclass
class RecipeSiteTestCase:
url: str
expected_slug: str
num_ingredients: int
num_steps: int
test_cases = [
RecipeSiteTestCase(
url="https://www.seriouseats.com/taiwanese-three-cup-chicken-san-bei-gi-recipe",
expected_slug="taiwanese-three-cup-chicken-san-bei-ji-recipe",
num_ingredients=10,
num_steps=3,
),
RecipeSiteTestCase(
url="https://www.rezeptwelt.de/backen-herzhaft-rezepte/schinken-kaese-waffeln-ohne-viel-schnickschnack/4j0bkiig-94d4d-106529-cfcd2-is97x2ml",
expected_slug="schinken-kase-waffeln-ohne-viel-schnickschnack",
num_ingredients=7,
num_steps=1, # Malformed JSON Data, can't parse steps just get one string
),
RecipeSiteTestCase(
url="https://cookpad.com/us/recipes/5544853-sous-vide-smoked-beef-ribs",
expected_slug="sous-vide-smoked-beef-ribs",
num_ingredients=7,
num_steps=12,
),
RecipeSiteTestCase(
url="https://www.greatbritishchefs.com/recipes/jam-roly-poly-recipe",
expected_slug="jam-roly-poly-with-custard",
num_ingredients=13,
num_steps=9,
),
RecipeSiteTestCase(
url="https://recipes.anovaculinary.com/recipe/sous-vide-shrimp",
expected_slug="sous-vide-shrimp",
num_ingredients=5,
num_steps=0,
),
RecipeSiteTestCase(
url="https://www.bonappetit.com/recipe/detroit-style-pepperoni-pizza",
expected_slug="detroit-style-pepperoni-pizza",
num_ingredients=8,
num_steps=5,
),
]
@pytest.mark.skip
@pytest.mark.parametrize("recipe_test_data", test_cases)
def test_recipe_parser(recipe_test_data: RecipeSiteTestCase):
recipe = scraper.create_from_url(recipe_test_data.url)

View File

@ -0,0 +1,11 @@
from pathlib import Path
from mealie.core import security
from mealie.routes.deps import validate_file_token
def test_create_file_token():
file_path = Path(__file__).parent
file_token = security.create_file_token(file_path)
assert file_path == validate_file_token(file_token)

3
tests/utils/helpers.py Normal file
View File

@ -0,0 +1,3 @@
class MatchAny:
def __eq__(self, _: object) -> bool:
return True

View File

@ -2,20 +2,50 @@ from dataclasses import dataclass
@dataclass
class RecipeTestData:
class RecipeSiteTestCase:
url: str
expected_slug: str
num_ingredients: int
num_steps: int
def build_recipe_store():
def get_recipe_test_cases():
return [
RecipeTestData(
url="https://www.bonappetit.com/recipe/spinach-thepla-and-vaghareli-dahi",
expected_slug="thepla-recipe-with-vaghareli-dahi",
RecipeSiteTestCase(
url="https://www.seriouseats.com/taiwanese-three-cup-chicken-san-bei-gi-recipe",
expected_slug="taiwanese-three-cup-chicken-san-bei-ji-recipe",
num_ingredients=10,
num_steps=3,
),
RecipeTestData(
url="https://www.bonappetit.com/recipe/classic-coleslaw",
expected_slug="traditional-coleslaw-recipe",
RecipeSiteTestCase(
url="https://www.rezeptwelt.de/backen-herzhaft-rezepte/schinken-kaese-waffeln-ohne-viel-schnickschnack/4j0bkiig-94d4d-106529-cfcd2-is97x2ml",
expected_slug="schinken-kase-waffeln-ohne-viel-schnickschnack",
num_ingredients=7,
num_steps=1, # Malformed JSON Data, can't parse steps just get one string
),
RecipeSiteTestCase(
url="https://cookpad.com/us/recipes/5544853-sous-vide-smoked-beef-ribs",
expected_slug="sous-vide-smoked-beef-ribs",
num_ingredients=7,
num_steps=12,
),
RecipeSiteTestCase(
url="https://www.greatbritishchefs.com/recipes/jam-roly-poly-recipe",
expected_slug="jam-roly-poly-with-custard",
num_ingredients=13,
num_steps=9,
),
RecipeSiteTestCase(
url="https://recipes.anovaculinary.com/recipe/sous-vide-shrimp",
expected_slug="sous-vide-shrimp",
num_ingredients=5,
num_steps=0,
),
RecipeSiteTestCase(
url="https://www.bonappetit.com/recipe/detroit-style-pepperoni-pizza",
expected_slug="detroit-style-pepperoni-pizza",
num_ingredients=8,
num_steps=5,
),
]