Skip to content

Comments

feat: Recipe management with ingredients, steps, images, and shopping list integration#64

Open
Copilot wants to merge 7 commits intomainfrom
copilot/add-recipe-creation-feature
Open

feat: Recipe management with ingredients, steps, images, and shopping list integration#64
Copilot wants to merge 7 commits intomainfrom
copilot/add-recipe-creation-feature

Conversation

Copy link
Contributor

Copilot AI commented Feb 23, 2026

  • Initial recipe feature implementation
  • Address PR review comments (validation, URL safety, format, db exception handling, integration tests)
  • Fix backend Release build: remove bin\Debug build artifacts accidentally committed to git
  • Add frontend tests: 204 tests pass, coverage 88.57%
  • Fix backend integration test isolation: add Recipes, RecipeIngredients, RecipeSteps, RecipeImages to ResetDatabaseAsync() in AnythingApiFactory.cs — tests were failing because recipe data created by one test persisted into the next test
Original prompt

This section details on the original issue you should resolve

<issue_title>Recipe</issue_title>
<issue_description>I want to be able to create recipes.
It should have a name, images, optional link, ingredients, steps and notes.

  1. Ingredients have also a unit and amount and optionally a group if the recipe consists of multiple parts
  2. Steps are for now just a simple list with text
  3. Notes are just a free text field
  4. Link is an external reference to where inspiration is from
  5. It should be possible to add the ingredients to a shopping list</issue_description>

Comments on the Issue (you are @copilot in this section)


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI and others added 3 commits February 23, 2026 13:52
- Add Recipe, RecipeIngredient, RecipeStep, RecipeImage entities to ApplicationDbContext
- Add EF Core model configuration (max lengths, FKs with cascade delete, soft delete)
- Create RecipeEndpoints.cs with full CRUD for recipes, ingredients, steps, images
- Add POST /api/recipes/{id}/add-to-shopping-list endpoint
- Register MapRecipeEndpoints() in Program.cs
- Add AddRecipes EF Core migration

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Append Recipe, RecipeIngredient, RecipeStep, RecipeImage interfaces and
  Create/Update request interfaces to models/index.ts with discriminator,
  deserialize, and serialize functions
- Create api/recipes/index.ts collection builder (GET list, POST create)
- Create api/recipes/item/index.ts item builder (GET, PUT void, DELETE)
  with navigation to ingredients, steps, images, addToShoppingList
- Create api/recipes/item/ingredients/index.ts and item/index.ts
- Create api/recipes/item/steps/index.ts and item/index.ts
- Create api/recipes/item/images/index.ts and item/index.ts (DELETE only)
- Create api/recipes/item/addToShoppingList/index.ts (POST with body)
- Update api/index.ts to add recipes property and navigation metadata

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add useRecipes.ts with hooks for recipes, ingredients, steps, images, and add-to-shopping-list
- Add useRecipes.test.tsx with 10 passing tests
- Add app/recipes/page.tsx (list page with create/delete)
- Add app/recipes/[id]/page.tsx (detail page with ingredients, steps, images, add-to-shopping-list)
- Update app/page.tsx to add Recipes navigation button

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI changed the title [WIP] Add feature to create recipes with details feat: Recipe management with ingredients, steps, images, and shopping list integration Feb 23, 2026
Copilot AI requested a review from inur93 February 23, 2026 14:09
@inur93 inur93 marked this pull request as ready for review February 23, 2026 21:52
@inur93 inur93 requested a review from Copilot February 23, 2026 21:52
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request implements a comprehensive recipe management system with full CRUD operations, nested resources (ingredients, steps, images), and shopping list integration. The feature allows users to create and manage recipes with structured data including ingredients (with amounts, units, and groups), ordered preparation steps, image URLs, and optional external links. The implementation follows the existing codebase patterns for soft deletes, authorization, and endpoint configuration.

Changes:

  • Backend: New Recipe entities with EF Core migration, 16 API endpoints for recipe CRUD and nested resources, plus shopping list integration endpoint
  • Frontend: Kiota-generated API client, React hooks for all operations, list and detail pages with edit modes, plus comprehensive unit tests
  • Integration: Added shopping list button to home page, ingredients can be bulk-added to any shopping list with automatic recommendation seeding

Reviewed changes

Copilot reviewed 24 out of 68 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
src/Anything.API/Program.cs Registers new RecipeEndpoints
src/Anything.API/Endpoints/RecipeEndpoints.cs Implements 16 API endpoints for recipes, ingredients, steps, images, and shopping list integration
src/Anything.API/Data/ApplicationDbContext.cs Adds Recipe, RecipeIngredient, RecipeStep, RecipeImage entities with configuration
src/Anything.API/Migrations/*.cs EF Core migration creating four new tables with foreign keys and indexes
anything-frontend/src/lib/api-client/* Manually generated Kiota API client for recipe endpoints
anything-frontend/src/hooks/useRecipes.ts 14 React hooks for recipe operations with React Query
anything-frontend/src/hooks/useRecipes.test.tsx Unit tests for recipe hooks
anything-frontend/src/app/recipes/page.tsx Recipe list page with create/delete functionality
anything-frontend/src/app/recipes/[id]/page.tsx Recipe detail page with edit mode for all nested resources
anything-frontend/src/app/page.tsx Adds Recipes navigation button
anything-frontend/package-lock.json Dependency updates (peer dependencies marking)
Files not reviewed (2)
  • anything-frontend/package-lock.json: Language not supported
  • src/Anything.API/Migrations/20260223134937_AddRecipes.Designer.cs: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 286 to 287
? $"{ingredient.Amount:G29} {ingredient.Name}"
: $"{ingredient.Amount:G29} {ingredient.Unit} {ingredient.Name}").ToList();
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The format specifier G29 is used for decimal formatting. While technically valid, the G format specifier typically only needs precision up to G29 for extreme cases. For recipe ingredient amounts, a more reasonable precision like G (default 7-15 significant digits) or an explicit format like 0.## would be clearer and more appropriate for typical cooking measurements. The G29 format may produce unnecessarily long decimal representations.

Suggested change
? $"{ingredient.Amount:G29} {ingredient.Name}"
: $"{ingredient.Amount:G29} {ingredient.Unit} {ingredient.Name}").ToList();
? $"{ingredient.Amount:0.##} {ingredient.Name}"
: $"{ingredient.Amount:0.##} {ingredient.Unit} {ingredient.Name}").ToList();

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 19ba997 — changed format specifier from G29 to 0.## for cleaner decimal representation in shopping list item names.

Comment on lines 469 to 470
<img
src={image.url ?? ""}
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The image URL is used directly in the src attribute without validation for URL schemes. Malicious users could potentially provide javascript: or data: URLs that could lead to XSS attacks. While React escapes attributes, it's safer to validate that URLs use safe schemes (http/https) on the backend or implement Content Security Policy (CSP) headers to block dangerous URL schemes.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 19ba997 — added an isSafeUrl() helper that validates the http:///https:// scheme before using any URL in <img src> or <a href> attributes. Unsafe URLs are replaced with an empty string or #.

Comment on lines 357 to 358
[Range(0, double.MaxValue, ErrorMessage = "Amount must be a non-negative number.")]
decimal Amount,
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The validation allows an amount of 0 for ingredients using [Range(0, double.MaxValue)]. This means recipes can have ingredients with 0 quantity, which doesn't make practical sense. Consider changing the minimum value to a small positive number (e.g., 0.001) or using [Range(0.001, double.MaxValue)] to ensure meaningful quantities.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 19ba997 — changed [Range(0, double.MaxValue)] to [Range(0.001, double.MaxValue)] on CreateRecipeIngredientRequest.Amount so zero quantities are rejected.

Comment on lines 328 to 329
[StringLength(500, ErrorMessage = "Link must be at most 500 characters.")]
string? Link,
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Link field for recipes lacks proper URL format validation. While there's a length constraint, there's no validation to ensure the string is actually a valid URL. Consider adding a [Url] validation attribute to ensure users provide valid URLs and maintain data integrity.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 19ba997 — added [Url(ErrorMessage = "Link must be a valid URL.")] to the Link field in CreateRecipeRequest.

Comment on lines +221 to +222
<a
href={recipe.link}
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The recipe link URL is used directly in an anchor tag's href attribute without validation for URL schemes. Malicious users could provide javascript: URLs that could lead to XSS attacks when clicked. While the rel="noopener noreferrer" mitigates some risks, it's safer to validate that URLs use safe schemes (http/https) on the backend or client-side before rendering.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 19ba997 — the isSafeUrl() helper validates the http:///https:// scheme before the link is rendered in <a href>. The backend [Url] attribute also now rejects non-URL values at the API level.

Comment on lines 1 to 321
using System.ComponentModel.DataAnnotations;
using Anything.API.Data;
using Microsoft.EntityFrameworkCore;
using MinimalApis.Extensions.Binding;

namespace Anything.API.Endpoints;

public static class RecipeEndpoints
{
private const string RecipeNotFound = "Recipe not found.";
private const string IngredientNotFound = "Ingredient not found.";
private const string StepNotFound = "Step not found.";
private const string ImageNotFound = "Image not found.";

public static void MapRecipeEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/recipes");

group.MapGet("/", async (ApplicationDbContext db) =>
{
return await db.Recipes
.Where(r => r.DeletedOn == null)
.ToListAsync();
})
.WithName("GetRecipes")
.RequireAuthorization();

group.MapGet("/{id}", async (int id, ApplicationDbContext db) =>
{
return await db.Recipes.FindAsync(id) is Recipe recipe && recipe.DeletedOn == null
? Results.Ok(recipe)
: Results.NotFound(RecipeNotFound);
})
.WithName("GetRecipeById")
.RequireAuthorization();

group.MapPost("/", async (CreateRecipeRequest request, ApplicationDbContext db) =>
{
var recipe = new Recipe
{
Name = request.Name,
Link = request.Link,
Notes = request.Notes
};

db.Recipes.Add(recipe);
await db.SaveChangesAsync();
return Results.Created($"/api/recipes/{recipe.Id}", recipe);
})
.WithName("CreateRecipe")
.WithParameterValidation()
.RequireAuthorization();

group.MapPut("/{id}", async (int id, UpdateRecipeRequest request, ApplicationDbContext db) =>
{
var recipe = await db.Recipes.FindAsync(id);
if (recipe is null || recipe.DeletedOn != null)
return Results.NotFound(RecipeNotFound);

recipe.Name = request.Name;
recipe.Link = request.Link;
recipe.Notes = request.Notes;
recipe.ModifiedOn = DateTime.UtcNow;

await db.SaveChangesAsync();
return Results.NoContent();
})
.WithName("UpdateRecipe")
.WithParameterValidation()
.RequireAuthorization();

group.MapDelete("/{id}", async (int id, ApplicationDbContext db) =>
{
var recipe = await db.Recipes.FindAsync(id);
if (recipe is null || recipe.DeletedOn != null)
return Results.NotFound(RecipeNotFound);

recipe.DeletedOn = DateTime.UtcNow;
await db.SaveChangesAsync();
return Results.NoContent();
})
.WithName("DeleteRecipe")
.RequireAuthorization();

// Ingredients
group.MapGet("/{id}/ingredients", async (int id, ApplicationDbContext db) =>
{
var recipe = await db.Recipes.FindAsync(id);
if (recipe is null || recipe.DeletedOn != null)
return Results.NotFound(RecipeNotFound);

var ingredients = await db.RecipeIngredients
.Where(i => i.RecipeId == id && i.DeletedOn == null)
.ToListAsync();
return Results.Ok(ingredients);
})
.WithName("GetRecipeIngredients")
.RequireAuthorization();

group.MapPost("/{id}/ingredients", async (int id, CreateRecipeIngredientRequest request, ApplicationDbContext db) =>
{
var recipe = await db.Recipes.FindAsync(id);
if (recipe is null || recipe.DeletedOn != null)
return Results.NotFound(RecipeNotFound);

var ingredient = new RecipeIngredient
{
RecipeId = id,
Name = request.Name,
Amount = request.Amount,
Unit = request.Unit,
Group = request.Group
};

db.RecipeIngredients.Add(ingredient);
await db.SaveChangesAsync();
return Results.Created($"/api/recipes/{id}/ingredients/{ingredient.Id}", ingredient);
})
.WithName("AddRecipeIngredient")
.WithParameterValidation()
.RequireAuthorization();

group.MapPut("/{id}/ingredients/{ingredientId}", async (int id, int ingredientId, UpdateRecipeIngredientRequest request, ApplicationDbContext db) =>
{
var ingredient = await db.RecipeIngredients.FindAsync(ingredientId);
if (ingredient is null || ingredient.DeletedOn != null || ingredient.RecipeId != id)
return Results.NotFound(IngredientNotFound);

ingredient.Name = request.Name;
ingredient.Amount = request.Amount;
ingredient.Unit = request.Unit;
ingredient.Group = request.Group;
ingredient.ModifiedOn = DateTime.UtcNow;

await db.SaveChangesAsync();
return Results.NoContent();
})
.WithName("UpdateRecipeIngredient")
.WithParameterValidation()
.RequireAuthorization();

group.MapDelete("/{id}/ingredients/{ingredientId}", async (int id, int ingredientId, ApplicationDbContext db) =>
{
var ingredient = await db.RecipeIngredients.FindAsync(ingredientId);
if (ingredient is null || ingredient.DeletedOn != null || ingredient.RecipeId != id)
return Results.NotFound(IngredientNotFound);

ingredient.DeletedOn = DateTime.UtcNow;
await db.SaveChangesAsync();
return Results.NoContent();
})
.WithName("DeleteRecipeIngredient")
.RequireAuthorization();

// Steps
group.MapGet("/{id}/steps", async (int id, ApplicationDbContext db) =>
{
var recipe = await db.Recipes.FindAsync(id);
if (recipe is null || recipe.DeletedOn != null)
return Results.NotFound(RecipeNotFound);

var steps = await db.RecipeSteps
.Where(s => s.RecipeId == id && s.DeletedOn == null)
.OrderBy(s => s.Order)
.ToListAsync();
return Results.Ok(steps);
})
.WithName("GetRecipeSteps")
.RequireAuthorization();

group.MapPost("/{id}/steps", async (int id, CreateRecipeStepRequest request, ApplicationDbContext db) =>
{
var recipe = await db.Recipes.FindAsync(id);
if (recipe is null || recipe.DeletedOn != null)
return Results.NotFound(RecipeNotFound);

var step = new RecipeStep
{
RecipeId = id,
Text = request.Text,
Order = request.Order
};

db.RecipeSteps.Add(step);
await db.SaveChangesAsync();
return Results.Created($"/api/recipes/{id}/steps/{step.Id}", step);
})
.WithName("AddRecipeStep")
.WithParameterValidation()
.RequireAuthorization();

group.MapPut("/{id}/steps/{stepId}", async (int id, int stepId, UpdateRecipeStepRequest request, ApplicationDbContext db) =>
{
var step = await db.RecipeSteps.FindAsync(stepId);
if (step is null || step.DeletedOn != null || step.RecipeId != id)
return Results.NotFound(StepNotFound);

step.Text = request.Text;
step.Order = request.Order;
step.ModifiedOn = DateTime.UtcNow;

await db.SaveChangesAsync();
return Results.NoContent();
})
.WithName("UpdateRecipeStep")
.WithParameterValidation()
.RequireAuthorization();

group.MapDelete("/{id}/steps/{stepId}", async (int id, int stepId, ApplicationDbContext db) =>
{
var step = await db.RecipeSteps.FindAsync(stepId);
if (step is null || step.DeletedOn != null || step.RecipeId != id)
return Results.NotFound(StepNotFound);

step.DeletedOn = DateTime.UtcNow;
await db.SaveChangesAsync();
return Results.NoContent();
})
.WithName("DeleteRecipeStep")
.RequireAuthorization();

// Images
group.MapGet("/{id}/images", async (int id, ApplicationDbContext db) =>
{
var recipe = await db.Recipes.FindAsync(id);
if (recipe is null || recipe.DeletedOn != null)
return Results.NotFound(RecipeNotFound);

var images = await db.RecipeImages
.Where(i => i.RecipeId == id && i.DeletedOn == null)
.ToListAsync();
return Results.Ok(images);
})
.WithName("GetRecipeImages")
.RequireAuthorization();

group.MapPost("/{id}/images", async (int id, CreateRecipeImageRequest request, ApplicationDbContext db) =>
{
var recipe = await db.Recipes.FindAsync(id);
if (recipe is null || recipe.DeletedOn != null)
return Results.NotFound(RecipeNotFound);

var image = new RecipeImage
{
RecipeId = id,
Url = request.Url
};

db.RecipeImages.Add(image);
await db.SaveChangesAsync();
return Results.Created($"/api/recipes/{id}/images/{image.Id}", image);
})
.WithName("AddRecipeImage")
.WithParameterValidation()
.RequireAuthorization();

group.MapDelete("/{id}/images/{imageId}", async (int id, int imageId, ApplicationDbContext db) =>
{
var image = await db.RecipeImages.FindAsync(imageId);
if (image is null || image.DeletedOn != null || image.RecipeId != id)
return Results.NotFound(ImageNotFound);

image.DeletedOn = DateTime.UtcNow;
await db.SaveChangesAsync();
return Results.NoContent();
})
.WithName("DeleteRecipeImage")
.RequireAuthorization();

// Add ingredients to shopping list
group.MapPost("/{id}/add-to-shopping-list", async (int id, AddToShoppingListRequest request, ApplicationDbContext db) =>
{
var recipe = await db.Recipes.FindAsync(id);
if (recipe is null || recipe.DeletedOn != null)
return Results.NotFound(RecipeNotFound);

var shoppingList = await db.ShoppingLists.FindAsync(request.ShoppingListId);
if (shoppingList is null || shoppingList.DeletedOn != null)
return Results.NotFound("Shopping list not found.");

var ingredients = await db.RecipeIngredients
.Where(i => i.RecipeId == id && i.DeletedOn == null)
.ToListAsync();

var itemNames = ingredients.Select(ingredient => string.IsNullOrWhiteSpace(ingredient.Unit)
? $"{ingredient.Amount:G29} {ingredient.Name}"
: $"{ingredient.Amount:G29} {ingredient.Unit} {ingredient.Name}").ToList();

var itemNamesLower = itemNames.Select(n => n.ToLower()).ToHashSet();
var existingRecommendations = await db.ShoppingListRecommendations
.Where(r => r.DeletedOn == null && itemNamesLower.Contains(r.Name.ToLower()))
.Select(r => r.Name.ToLower())
.ToHashSetAsync();

foreach (var itemName in itemNames)
{
db.ShoppingListItems.Add(new ShoppingListItem
{
ShoppingListId = request.ShoppingListId,
Name = itemName
});

var nameNormalized = itemName.Trim();
if (!existingRecommendations.Contains(nameNormalized.ToLower()))
{
db.ShoppingListRecommendations.Add(new ShoppingListRecommendation
{
Name = nameNormalized,
IsApproved = false
});
existingRecommendations.Add(nameNormalized.ToLower());
}
}

await db.SaveChangesAsync();
return Results.NoContent();
})
.WithName("AddRecipeIngredientsToShoppingList")
.WithParameterValidation()
.RequireAuthorization();
}
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The codebase has comprehensive integration tests for other endpoints (e.g., InventoryBoxEndpointTests, ShoppingListRecommendationEndpointTests), but there are no integration tests for the new RecipeEndpoints. Consider adding integration tests following the pattern established in tests/Anything.API.IntegrationTests/ to ensure the Recipe API endpoints work correctly, including nested resources (ingredients, steps, images) and the shopping list integration functionality.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added RecipeEndpointTests.cs in 19ba997 with 30 integration tests following the existing ShoppingListRecommendationEndpointTests pattern (raw HttpClient). Tests cover all endpoints: recipe CRUD, ingredients (including 0/negative amount rejection and [Url] rejection for images/links), steps, images, and the add-to-shopping-list action (including 404 for missing recipe/list).

Comment on lines 346 to 347
[Range(0, double.MaxValue, ErrorMessage = "Amount must be a non-negative number.")]
decimal Amount,
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The validation allows an amount of 0 for ingredients using [Range(0, double.MaxValue)]. This means recipes can have ingredients with 0 quantity, which doesn't make practical sense. Consider changing the minimum value to a small positive number (e.g., 0.001) or using [Range(0.001, double.MaxValue)] to ensure meaningful quantities.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 19ba997 — changed [Range(0, double.MaxValue)] to [Range(0.001, double.MaxValue)] on UpdateRecipeIngredientRequest.Amount as well.

}
}

await db.SaveChangesAsync();
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a potential database constraint violation issue here. The ShoppingListRecommendation table has a unique constraint on the Name column (line 128 in ApplicationDbContext.cs). When multiple ingredients are added in a loop and recommendations are created, a race condition could occur if the same recommendation name appears multiple times in the itemNames list. While the code maintains an in-memory set (existingRecommendations) to prevent duplicates within this request, it should wrap the SaveChangesAsync in a try-catch to handle potential unique constraint violations gracefully, or query for existing recommendations by all itemNames upfront before the loop.

Suggested change
await db.SaveChangesAsync();
try
{
await db.SaveChangesAsync();
}
catch (DbUpdateException)
{
// Handle potential unique constraint violations (e.g., on ShoppingListRecommendation.Name)
return Results.Problem("A database error occurred while saving the shopping list items and recommendations.");
}

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 19ba997 — wrapped SaveChangesAsync in try-catch (DbUpdateException) and return Results.Problem(...) on failure. The in-memory existingRecommendations set still prevents duplicates within a single request; the try-catch handles any edge-case race conditions at the DB layer.


public record CreateRecipeImageRequest(
[Required(ErrorMessage = "Url is required.")]
[StringLength(1000, MinimumLength = 1, ErrorMessage = "Url must be between 1 and 1000 characters.")]
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The URL field for recipe images lacks proper URL format validation. While there's a length constraint, there's no validation to ensure the string is actually a valid URL. Consider adding a [Url] validation attribute to ensure users provide valid URLs, preventing broken image links and potential issues.

Suggested change
[StringLength(1000, MinimumLength = 1, ErrorMessage = "Url must be between 1 and 1000 characters.")]
[StringLength(1000, MinimumLength = 1, ErrorMessage = "Url must be between 1 and 1000 characters.")]
[Url(ErrorMessage = "Url must be a valid URL.")]

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 19ba997 — added [Url(ErrorMessage = "Url must be a valid URL.")] to CreateRecipeImageRequest.Url. The integration tests also verify that an invalid URL returns HTTP 400.

Comment on lines 337 to 338
[StringLength(500, ErrorMessage = "Link must be at most 500 characters.")]
string? Link,
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Link field for recipes lacks proper URL format validation. While there's a length constraint, there's no validation to ensure the string is actually a valid URL. Consider adding a [Url] validation attribute to ensure users provide valid URLs and maintain data integrity.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 19ba997 — added [Url(ErrorMessage = "Link must be a valid URL.")] to the Link field in UpdateRecipeRequest as well.

Copy link
Contributor

@inur93 inur93 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please solve comments and make sure tests are green and coverage is over 80%

… integration tests

Co-authored-by: inur93 <5747088+inur93@users.noreply.github.com>
Copilot AI requested a review from inur93 February 23, 2026 22:26
Copy link
Contributor

@inur93 inur93 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please solve build and test issues and make sure frontend coverage is over 80%

- Create src/app/recipes/page.test.tsx with 14 tests (loading, error,
  empty state, list display, create, delete, navigation, admin panel)
- Create src/app/recipes/[id]/page.test.tsx with 20 tests (loading, error,
  recipe details, edit mode, ingredients CRUD, steps CRUD, images CRUD,
  back navigation, add-to-shopping-list)
- Extend src/hooks/useRecipes.test.tsx with 9 new tests covering
  useRecipe, useUpdateRecipe, useUpdateRecipeIngredient,
  useDeleteRecipeIngredient, useUpdateRecipeStep, useDeleteRecipeStep,
  useAddRecipeImage, useDeleteRecipeImage

Coverage improvements:
- useRecipes.ts: 53.8% -> 100%
- recipes/page.tsx: 0% -> 95.13%
- recipes/[id]/page.tsx: 0% -> 87.59%
- Overall statements: 64.12% -> 88.57%

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Feb 23, 2026

Please solve build and test issues and make sure frontend coverage is over 80%

Fixed in 1bb1168:

Backend build: The CI failure (MSB3552: Resource file "**/*.resx" cannot be found) was caused by a bin\Debug directory (Windows-style backslash as part of the directory name) that was accidentally committed to git. On Linux, MSBuild's glob expander tries to normalize bin\Debugbin/Debug, fails with DirectoryNotFoundException, and falls back to using the literal glob as a filename, causing the build error. Removed the committed binary build artifacts.

Frontend coverage: Added 45 new tests across 3 files:

  • src/app/recipes/page.test.tsx — 14 tests (0% → 95%)
  • src/app/recipes/[id]/page.test.tsx — 20 tests (0% → 88%)
  • Extended src/hooks/useRecipes.test.tsx — 9 new tests (54% → 100%)

Overall coverage: 88.57% (was 64.12%), all 204 tests pass.

Copilot AI requested a review from inur93 February 23, 2026 22:54
Copy link
Contributor

@inur93 inur93 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Backend Tests are failing

…isolation

Co-authored-by: inur93 <5747088+inur93@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Feb 24, 2026

Backend Tests are failing

Fixed in dc3177d. The ResetDatabaseAsync() method in AnythingApiFactory.cs was missing the four new Recipe tables (Recipes, RecipeIngredients, RecipeSteps, RecipeImages), so recipe data created by one test persisted into subsequent tests. Added the missing RemoveRange calls for all four tables.

Copilot AI requested a review from inur93 February 24, 2026 10:19
@sonarqubecloud
Copy link

Quality Gate Failed Quality Gate failed for 'Anything Frontend'

Failed conditions
78.7% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

@sonarqubecloud
Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Recipe

2 participants