Add product catalog with per-store section memory
Introduces a global Products catalog plus per-family overrides and private FamilyProducts, exposed via /api/products with a merged search. Shopping list items and recipe ingredients gain optional ProductId/FamilyProductId links, and a new ProductStoreSection table remembers which section a product was last placed in at a given store so future adds auto-assign the right section. Frontend gets a reusable ProductTypeahead component, wired into list-item add and recipe ingredient entry with free-form fallback. A startup CatalogSeeder loads ~115 curated staples from an embedded JSON resource via INSERT ... ON CONFLICT DO NOTHING; skipped under the Testing environment so integration tests keep a clean slate.
This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using YesChef.Api.Entities;
|
||||
using YesChef.Api.Features.Products;
|
||||
using YesChef.Api.IntegrationTests.Infrastructure;
|
||||
|
||||
namespace YesChef.Api.IntegrationTests.Features;
|
||||
|
||||
public class ProductEndpointsTests : AuthenticatedIntegrationTest
|
||||
{
|
||||
private Task<int> GetFamilyIdAsync() => UseDbAsync(db =>
|
||||
db.FamilyMemberships.Where(m => m.UserId == User.Id).Select(m => m.FamilyId).SingleAsync());
|
||||
|
||||
[Test]
|
||||
public async Task Search_returns_global_and_family_products_merged()
|
||||
{
|
||||
var familyId = await GetFamilyIdAsync();
|
||||
await UseDbAsync(async db =>
|
||||
{
|
||||
db.Products.Add(new Product { Name = "Bananas" });
|
||||
db.Products.Add(new Product { Name = "Carrots" });
|
||||
db.FamilyProducts.Add(new FamilyProduct { FamilyId = familyId, Name = "House Bread" });
|
||||
await db.SaveChangesAsync();
|
||||
});
|
||||
|
||||
var results = await Client.GetFromJsonAsync<List<ProductEndpoints.ProductDto>>("/api/products?q=");
|
||||
|
||||
await Assert.That(results!.Select(r => r.Name)).IsEquivalentTo(new[] { "Bananas", "Carrots", "House Bread" });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Search_filters_case_insensitively_on_effective_name()
|
||||
{
|
||||
await UseDbAsync(async db =>
|
||||
{
|
||||
db.Products.Add(new Product { Name = "Bananas" });
|
||||
db.Products.Add(new Product { Name = "Apples" });
|
||||
await db.SaveChangesAsync();
|
||||
});
|
||||
|
||||
var results = await Client.GetFromJsonAsync<List<ProductEndpoints.ProductDto>>("/api/products?q=ban");
|
||||
|
||||
await Assert.That(results!.Select(r => r.Name)).IsEquivalentTo(new[] { "Bananas" });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Search_applies_family_override_to_global_product()
|
||||
{
|
||||
var familyId = await GetFamilyIdAsync();
|
||||
Product apples = null!;
|
||||
await UseDbAsync(async db =>
|
||||
{
|
||||
apples = new Product { Name = "Apples", Brand = "Generic" };
|
||||
db.Products.Add(apples);
|
||||
await db.SaveChangesAsync();
|
||||
db.FamilyProductOverrides.Add(new FamilyProductOverride
|
||||
{
|
||||
FamilyId = familyId,
|
||||
ProductId = apples.Id,
|
||||
Brand = "Honeycrisp",
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
});
|
||||
|
||||
var results = await Client.GetFromJsonAsync<List<ProductEndpoints.ProductDto>>("/api/products?q=apple");
|
||||
|
||||
var dto = results!.Single();
|
||||
await Assert.That(dto.Name).IsEqualTo("Apples");
|
||||
await Assert.That(dto.Brand).IsEqualTo("Honeycrisp");
|
||||
await Assert.That(dto.IsOverridden).IsTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Search_does_not_leak_other_family_private_products()
|
||||
{
|
||||
await UseDbAsync(async db =>
|
||||
{
|
||||
var otherFamily = new Family { Name = "Other", InviteCode = "other-code" };
|
||||
db.Families.Add(otherFamily);
|
||||
await db.SaveChangesAsync();
|
||||
db.FamilyProducts.Add(new FamilyProduct { FamilyId = otherFamily.Id, Name = "Other Family Bread" });
|
||||
await db.SaveChangesAsync();
|
||||
});
|
||||
|
||||
var results = await Client.GetFromJsonAsync<List<ProductEndpoints.ProductDto>>("/api/products?q=bread");
|
||||
|
||||
await Assert.That(results!).IsEmpty();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Search_does_not_leak_other_family_overrides()
|
||||
{
|
||||
await UseDbAsync(async db =>
|
||||
{
|
||||
var apples = new Product { Name = "Apples", Brand = "Generic" };
|
||||
db.Products.Add(apples);
|
||||
var otherFamily = new Family { Name = "Other", InviteCode = "other-code" };
|
||||
db.Families.Add(otherFamily);
|
||||
await db.SaveChangesAsync();
|
||||
db.FamilyProductOverrides.Add(new FamilyProductOverride
|
||||
{
|
||||
FamilyId = otherFamily.Id,
|
||||
ProductId = apples.Id,
|
||||
Brand = "Their Honeycrisp",
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
});
|
||||
|
||||
var results = await Client.GetFromJsonAsync<List<ProductEndpoints.ProductDto>>("/api/products?q=apple");
|
||||
|
||||
var dto = results!.Single();
|
||||
await Assert.That(dto.Brand).IsEqualTo("Generic");
|
||||
await Assert.That(dto.IsOverridden).IsFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Create_persists_family_product_and_returns_201()
|
||||
{
|
||||
var response = await Client.PostAsJsonAsync("/api/products",
|
||||
new ProductEndpoints.CreateProductRequest("Sourdough", "Local Bakery", null));
|
||||
|
||||
await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.Created);
|
||||
var dto = await response.Content.ReadFromJsonAsync<ProductEndpoints.ProductDto>();
|
||||
await Assert.That(dto!.Kind).IsEqualTo(ProductEndpoints.ProductKind.Family);
|
||||
await Assert.That(dto.Brand).IsEqualTo("Local Bakery");
|
||||
|
||||
var persisted = await UseDbAsync(db => db.FamilyProducts.SingleAsync());
|
||||
await Assert.That(persisted.Name).IsEqualTo("Sourdough");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Create_returns_409_for_duplicate_name_within_family()
|
||||
{
|
||||
await Client.PostAsJsonAsync("/api/products",
|
||||
new ProductEndpoints.CreateProductRequest("Sourdough", null, null));
|
||||
|
||||
var response = await Client.PostAsJsonAsync("/api/products",
|
||||
new ProductEndpoints.CreateProductRequest("Sourdough", null, null));
|
||||
|
||||
await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.Conflict);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Create_returns_400_when_name_missing()
|
||||
{
|
||||
var response = await Client.PostAsJsonAsync("/api/products",
|
||||
new ProductEndpoints.CreateProductRequest(" ", null, null));
|
||||
|
||||
await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.BadRequest);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Update_family_product_changes_fields()
|
||||
{
|
||||
var familyId = await GetFamilyIdAsync();
|
||||
var product = await UseDbAsync(async db =>
|
||||
{
|
||||
var p = new FamilyProduct { FamilyId = familyId, Name = "Old", Brand = "OldBrand" };
|
||||
db.FamilyProducts.Add(p);
|
||||
await db.SaveChangesAsync();
|
||||
return p;
|
||||
});
|
||||
|
||||
var response = await Client.PutAsJsonAsync($"/api/products/family/{product.Id}",
|
||||
new ProductEndpoints.UpdateProductRequest("New", "NewBrand", "New notes"));
|
||||
|
||||
await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.OK);
|
||||
var refreshed = await UseDbAsync(db => db.FamilyProducts.SingleAsync(p => p.Id == product.Id));
|
||||
await Assert.That(refreshed.Name).IsEqualTo("New");
|
||||
await Assert.That(refreshed.Brand).IsEqualTo("NewBrand");
|
||||
await Assert.That(refreshed.Notes).IsEqualTo("New notes");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Update_family_product_404_for_unknown_id()
|
||||
{
|
||||
var response = await Client.PutAsJsonAsync("/api/products/family/99999",
|
||||
new ProductEndpoints.UpdateProductRequest("x", null, null));
|
||||
|
||||
await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.NotFound);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Update_global_product_creates_override_for_this_family_only()
|
||||
{
|
||||
var familyId = await GetFamilyIdAsync();
|
||||
var apples = await UseDbAsync(async db =>
|
||||
{
|
||||
var p = new Product { Name = "Apples", Brand = "Generic" };
|
||||
db.Products.Add(p);
|
||||
await db.SaveChangesAsync();
|
||||
return p;
|
||||
});
|
||||
|
||||
var response = await Client.PutAsJsonAsync($"/api/products/global/{apples.Id}",
|
||||
new ProductEndpoints.UpdateProductRequest(null, "Honeycrisp", null));
|
||||
|
||||
await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.OK);
|
||||
var dto = await response.Content.ReadFromJsonAsync<ProductEndpoints.ProductDto>();
|
||||
await Assert.That(dto!.Brand).IsEqualTo("Honeycrisp");
|
||||
await Assert.That(dto.IsOverridden).IsTrue();
|
||||
|
||||
// Global row untouched.
|
||||
var global = await UseDbAsync(db => db.Products.SingleAsync(p => p.Id == apples.Id));
|
||||
await Assert.That(global.Brand).IsEqualTo("Generic");
|
||||
|
||||
// Override stored under the caller's family.
|
||||
var ovr = await UseDbAsync(db => db.FamilyProductOverrides.SingleAsync());
|
||||
await Assert.That(ovr.FamilyId).IsEqualTo(familyId);
|
||||
await Assert.That(ovr.Brand).IsEqualTo("Honeycrisp");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Update_global_product_upserts_existing_override()
|
||||
{
|
||||
var familyId = await GetFamilyIdAsync();
|
||||
var apples = await UseDbAsync(async db =>
|
||||
{
|
||||
var p = new Product { Name = "Apples", Brand = "Generic" };
|
||||
db.Products.Add(p);
|
||||
await db.SaveChangesAsync();
|
||||
db.FamilyProductOverrides.Add(new FamilyProductOverride
|
||||
{
|
||||
FamilyId = familyId,
|
||||
ProductId = p.Id,
|
||||
Brand = "Honeycrisp",
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
return p;
|
||||
});
|
||||
|
||||
var response = await Client.PutAsJsonAsync($"/api/products/global/{apples.Id}",
|
||||
new ProductEndpoints.UpdateProductRequest(null, "Pink Lady", "Family favorite"));
|
||||
|
||||
await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.OK);
|
||||
var ovr = await UseDbAsync(db => db.FamilyProductOverrides.SingleAsync());
|
||||
await Assert.That(ovr.Brand).IsEqualTo("Pink Lady");
|
||||
await Assert.That(ovr.Notes).IsEqualTo("Family favorite");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Update_global_product_404_for_unknown_id()
|
||||
{
|
||||
var response = await Client.PutAsJsonAsync("/api/products/global/99999",
|
||||
new ProductEndpoints.UpdateProductRequest(null, "x", null));
|
||||
|
||||
await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.NotFound);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Endpoints_require_authentication()
|
||||
{
|
||||
var response = await AnonymousClient.GetAsync("/api/products");
|
||||
await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.Unauthorized);
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,67 @@ public class RecipeEndpointsTests : AuthenticatedIntegrationTest
|
||||
await Assert.That(recipe.Ingredients.Count).IsEqualTo(2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Create_links_ingredient_to_global_product()
|
||||
{
|
||||
var product = await UseDbAsync(async db =>
|
||||
{
|
||||
var p = new Product { Name = "Pasta" };
|
||||
db.Products.Add(p);
|
||||
await db.SaveChangesAsync();
|
||||
return p;
|
||||
});
|
||||
|
||||
var request = new RecipeEndpoints.CreateRecipeRequest(
|
||||
Title: "Pasta",
|
||||
Description: null,
|
||||
Instructions: null,
|
||||
Servings: null,
|
||||
SourceUrl: null,
|
||||
Ingredients: [new RecipeEndpoints.IngredientRequest("pasta", "1 lb", 1, ProductId: product.Id)]);
|
||||
|
||||
var response = await Client.PostAsJsonAsync("/api/recipes", request);
|
||||
|
||||
await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.Created);
|
||||
var ingredient = await UseDbAsync(db => db.RecipeIngredients.SingleAsync());
|
||||
await Assert.That(ingredient.ProductId).IsEqualTo(product.Id);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Create_rejects_ingredient_with_both_product_ids()
|
||||
{
|
||||
var request = new RecipeEndpoints.CreateRecipeRequest(
|
||||
Title: "x", Description: null, Instructions: null, Servings: null, SourceUrl: null,
|
||||
Ingredients: [new RecipeEndpoints.IngredientRequest("x", null, 1, ProductId: 1, FamilyProductId: 1)]);
|
||||
|
||||
var response = await Client.PostAsJsonAsync("/api/recipes", request);
|
||||
|
||||
await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.BadRequest);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Create_rejects_ingredient_with_other_familys_family_product()
|
||||
{
|
||||
var foreignProductId = await UseDbAsync(async db =>
|
||||
{
|
||||
var otherFamily = new Family { Name = "Other", InviteCode = "other-code" };
|
||||
db.Families.Add(otherFamily);
|
||||
await db.SaveChangesAsync();
|
||||
var p = new FamilyProduct { FamilyId = otherFamily.Id, Name = "Theirs" };
|
||||
db.FamilyProducts.Add(p);
|
||||
await db.SaveChangesAsync();
|
||||
return p.Id;
|
||||
});
|
||||
|
||||
var request = new RecipeEndpoints.CreateRecipeRequest(
|
||||
Title: "x", Description: null, Instructions: null, Servings: null, SourceUrl: null,
|
||||
Ingredients: [new RecipeEndpoints.IngredientRequest("x", null, 1, FamilyProductId: foreignProductId)]);
|
||||
|
||||
var response = await Client.PostAsJsonAsync("/api/recipes", request);
|
||||
|
||||
await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.BadRequest);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Get_by_id_returns_full_recipe_including_creator_and_ingredients()
|
||||
{
|
||||
|
||||
@@ -140,6 +140,199 @@ public class ShoppingListEndpointsTests : AuthenticatedIntegrationTest
|
||||
await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.NotFound);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Add_item_links_global_product_when_id_supplied()
|
||||
{
|
||||
var list = await CreateListAsync();
|
||||
var product = await UseDbAsync(async db =>
|
||||
{
|
||||
var p = new Product { Name = "Bananas" };
|
||||
db.Products.Add(p);
|
||||
await db.SaveChangesAsync();
|
||||
return p;
|
||||
});
|
||||
|
||||
var response = await Client.PostAsJsonAsync($"/api/lists/{list.Id}/items",
|
||||
new ShoppingListEndpoints.AddItemRequest("Bananas", ProductId: product.Id));
|
||||
|
||||
await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.Created);
|
||||
var item = await UseDbAsync(db => db.ShoppingListItems.SingleAsync());
|
||||
await Assert.That(item.ProductId).IsEqualTo(product.Id);
|
||||
await Assert.That(item.FamilyProductId).IsNull();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Add_item_links_family_product_when_id_supplied()
|
||||
{
|
||||
var list = await CreateListAsync();
|
||||
var familyId = await UseDbAsync(db =>
|
||||
db.FamilyMemberships.Where(m => m.UserId == User.Id).Select(m => m.FamilyId).SingleAsync());
|
||||
var familyProduct = await UseDbAsync(async db =>
|
||||
{
|
||||
var p = new FamilyProduct { FamilyId = familyId, Name = "House Bread" };
|
||||
db.FamilyProducts.Add(p);
|
||||
await db.SaveChangesAsync();
|
||||
return p;
|
||||
});
|
||||
|
||||
var response = await Client.PostAsJsonAsync($"/api/lists/{list.Id}/items",
|
||||
new ShoppingListEndpoints.AddItemRequest("House Bread", FamilyProductId: familyProduct.Id));
|
||||
|
||||
await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.Created);
|
||||
var item = await UseDbAsync(db => db.ShoppingListItems.SingleAsync());
|
||||
await Assert.That(item.FamilyProductId).IsEqualTo(familyProduct.Id);
|
||||
await Assert.That(item.ProductId).IsNull();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Add_item_rejects_setting_both_product_ids()
|
||||
{
|
||||
var list = await CreateListAsync();
|
||||
|
||||
var response = await Client.PostAsJsonAsync($"/api/lists/{list.Id}/items",
|
||||
new ShoppingListEndpoints.AddItemRequest("eggs", ProductId: 1, FamilyProductId: 1));
|
||||
|
||||
await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.BadRequest);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Add_item_rejects_unknown_product_id()
|
||||
{
|
||||
var list = await CreateListAsync();
|
||||
|
||||
var response = await Client.PostAsJsonAsync($"/api/lists/{list.Id}/items",
|
||||
new ShoppingListEndpoints.AddItemRequest("eggs", ProductId: 99999));
|
||||
|
||||
await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.BadRequest);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Add_item_records_section_memory_and_auto_assigns_on_next_add()
|
||||
{
|
||||
var list = await CreateListAsync();
|
||||
var section = await UseDbAsync(async db =>
|
||||
{
|
||||
var s = new StoreSection { FamilyId = Store.FamilyId, StoreId = Store.Id, Name = "Produce", SortOrder = 1 };
|
||||
db.StoreSections.Add(s);
|
||||
await db.SaveChangesAsync();
|
||||
return s;
|
||||
});
|
||||
var product = await UseDbAsync(async db =>
|
||||
{
|
||||
var p = new Product { Name = "Bananas-Memory" };
|
||||
db.Products.Add(p);
|
||||
await db.SaveChangesAsync();
|
||||
return p;
|
||||
});
|
||||
|
||||
// First add: explicit section + product → memory recorded.
|
||||
var first = await Client.PostAsJsonAsync($"/api/lists/{list.Id}/items",
|
||||
new ShoppingListEndpoints.AddItemRequest("Bananas", SectionId: section.Id, ProductId: product.Id));
|
||||
await Assert.That(first.StatusCode).IsEqualTo(HttpStatusCode.Created);
|
||||
|
||||
// Second add: same product, no section → auto-assigned from memory.
|
||||
var second = await Client.PostAsJsonAsync($"/api/lists/{list.Id}/items",
|
||||
new ShoppingListEndpoints.AddItemRequest("More Bananas", ProductId: product.Id));
|
||||
await Assert.That(second.StatusCode).IsEqualTo(HttpStatusCode.Created);
|
||||
|
||||
var items = await UseDbAsync(db => db.ShoppingListItems
|
||||
.Where(i => i.ShoppingListId == list.Id).OrderBy(i => i.Id).ToListAsync());
|
||||
await Assert.That(items.Count).IsEqualTo(2);
|
||||
await Assert.That(items[1].SectionId).IsEqualTo(section.Id);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Patch_item_section_updates_memory_for_next_add()
|
||||
{
|
||||
var list = await CreateListAsync();
|
||||
var (originalSection, correctedSection) = await UseDbAsync(async db =>
|
||||
{
|
||||
var s1 = new StoreSection { FamilyId = Store.FamilyId, StoreId = Store.Id, Name = "Pantry", SortOrder = 1 };
|
||||
var s2 = new StoreSection { FamilyId = Store.FamilyId, StoreId = Store.Id, Name = "Bakery", SortOrder = 2 };
|
||||
db.StoreSections.AddRange(s1, s2);
|
||||
await db.SaveChangesAsync();
|
||||
return (s1, s2);
|
||||
});
|
||||
var product = await UseDbAsync(async db =>
|
||||
{
|
||||
var p = new Product { Name = "Bread-Memory" };
|
||||
db.Products.Add(p);
|
||||
await db.SaveChangesAsync();
|
||||
return p;
|
||||
});
|
||||
|
||||
// Add with section A; that records (Product → A) memory.
|
||||
var addResponse = await Client.PostAsJsonAsync($"/api/lists/{list.Id}/items",
|
||||
new ShoppingListEndpoints.AddItemRequest("Bread", SectionId: originalSection.Id, ProductId: product.Id));
|
||||
var addBody = await addResponse.Content.ReadFromJsonAsync<JsonElement>();
|
||||
var firstItemId = addBody.GetProperty("id").GetInt32();
|
||||
|
||||
// User corrects to section B → memory should update.
|
||||
var patchResponse = await Client.PatchAsJsonAsync($"/api/lists/{list.Id}/items/{firstItemId}/section",
|
||||
new ShoppingListEndpoints.SetItemSectionRequest(correctedSection.Id));
|
||||
await Assert.That(patchResponse.StatusCode).IsEqualTo(HttpStatusCode.OK);
|
||||
|
||||
// Next add (no section) should now pull section B.
|
||||
var second = await Client.PostAsJsonAsync($"/api/lists/{list.Id}/items",
|
||||
new ShoppingListEndpoints.AddItemRequest("More Bread", ProductId: product.Id));
|
||||
var secondBody = await second.Content.ReadFromJsonAsync<JsonElement>();
|
||||
await Assert.That(secondBody.GetProperty("sectionId").GetInt32()).IsEqualTo(correctedSection.Id);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Section_memory_is_scoped_per_store()
|
||||
{
|
||||
var listA = await CreateListAsync();
|
||||
var otherStore = await Data.CreateStoreAsync(b => b.Named("Other Store"));
|
||||
var listB = await Data.CreateListAsync(b => b.ForStore(otherStore).CreatedBy(User));
|
||||
var (sectionA, sectionB) = await UseDbAsync(async db =>
|
||||
{
|
||||
var a = new StoreSection { FamilyId = Store.FamilyId, StoreId = Store.Id, Name = "Produce A", SortOrder = 1 };
|
||||
var b = new StoreSection { FamilyId = otherStore.FamilyId, StoreId = otherStore.Id, Name = "Produce B", SortOrder = 1 };
|
||||
db.StoreSections.AddRange(a, b);
|
||||
await db.SaveChangesAsync();
|
||||
return (a, b);
|
||||
});
|
||||
var product = await UseDbAsync(async db =>
|
||||
{
|
||||
var p = new Product { Name = "Apples-Memory" };
|
||||
db.Products.Add(p);
|
||||
await db.SaveChangesAsync();
|
||||
return p;
|
||||
});
|
||||
|
||||
// Memorize at store A only.
|
||||
await Client.PostAsJsonAsync($"/api/lists/{listA.Id}/items",
|
||||
new ShoppingListEndpoints.AddItemRequest("Apples", SectionId: sectionA.Id, ProductId: product.Id));
|
||||
|
||||
// Add at store B with no section → no memory yet for store B.
|
||||
var response = await Client.PostAsJsonAsync($"/api/lists/{listB.Id}/items",
|
||||
new ShoppingListEndpoints.AddItemRequest("Apples", ProductId: product.Id));
|
||||
var body = await response.Content.ReadFromJsonAsync<JsonElement>();
|
||||
await Assert.That(body.GetProperty("sectionId").ValueKind).IsEqualTo(JsonValueKind.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Add_item_rejects_other_familys_family_product()
|
||||
{
|
||||
var list = await CreateListAsync();
|
||||
var foreignProductId = await UseDbAsync(async db =>
|
||||
{
|
||||
var otherFamily = new Family { Name = "Other", InviteCode = "other-code" };
|
||||
db.Families.Add(otherFamily);
|
||||
await db.SaveChangesAsync();
|
||||
var p = new FamilyProduct { FamilyId = otherFamily.Id, Name = "Theirs" };
|
||||
db.FamilyProducts.Add(p);
|
||||
await db.SaveChangesAsync();
|
||||
return p.Id;
|
||||
});
|
||||
|
||||
var response = await Client.PostAsJsonAsync($"/api/lists/{list.Id}/items",
|
||||
new ShoppingListEndpoints.AddItemRequest("eggs", FamilyProductId: foreignProductId));
|
||||
|
||||
await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.BadRequest);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Check_toggles_item_state_and_attributes_to_user()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user