|
- using System.Net;
- using System.Text.RegularExpressions;
-
- namespace CartWise.Web.Tests;
-
- public class ShoppingListItemActionsTests : IClassFixture<CartWiseWebApplicationFactory>
- {
- private readonly CartWiseWebApplicationFactory _factory;
-
- public ShoppingListItemActionsTests(CartWiseWebApplicationFactory factory)
- {
- _factory = factory;
- }
-
- [Fact]
- public async Task TogglePurchased_MarksItemPurchased()
- {
- var client = _factory.CreateClient();
- await WebTestHelpers.RegisterAsync(client, "toggle1@example.com", "Toggle User");
- await WebTestHelpers.CreateHouseholdAsync(client, "Toggle Household");
- await AddItemAsync(client, "Milk");
-
- var listHtml = await client.GetStringAsync("/list");
- var (id, rowVersion) = ExtractFirstItemInfo(listHtml);
- var token = WebTestHelpers.ExtractAntiForgeryToken(listHtml);
-
- var response = await client.PostAsync($"/list/items/{id}/toggle", new FormUrlEncodedContent(new Dictionary<string, string>
- {
- ["__RequestVerificationToken"] = token,
- ["rowVersion"] = rowVersion
- }));
- response.EnsureSuccessStatusCode();
-
- var updatedHtml = await client.GetStringAsync("/list");
- HtmlAssert.Contains("Purchased", updatedHtml);
- }
-
- [Fact]
- public async Task TogglePurchased_TwiceReturnsItemToNeeded()
- {
- var client = _factory.CreateClient();
- await WebTestHelpers.RegisterAsync(client, "toggle2@example.com", "Toggle User 2");
- await WebTestHelpers.CreateHouseholdAsync(client, "Toggle Household 2");
- await AddItemAsync(client, "Milk");
-
- var listHtml = await client.GetStringAsync("/list");
- var (id, rowVersion) = ExtractFirstItemInfo(listHtml);
- var token = WebTestHelpers.ExtractAntiForgeryToken(listHtml);
-
- await client.PostAsync($"/list/items/{id}/toggle", new FormUrlEncodedContent(new Dictionary<string, string>
- {
- ["__RequestVerificationToken"] = token,
- ["rowVersion"] = rowVersion
- }));
-
- var midHtml = await client.GetStringAsync("/list");
- var (_, secondRowVersion) = ExtractFirstItemInfo(midHtml);
- var midToken = WebTestHelpers.ExtractAntiForgeryToken(midHtml);
-
- await client.PostAsync($"/list/items/{id}/toggle", new FormUrlEncodedContent(new Dictionary<string, string>
- {
- ["__RequestVerificationToken"] = midToken,
- ["rowVersion"] = secondRowVersion
- }));
-
- var finalHtml = await client.GetStringAsync("/list");
- HtmlAssert.Contains("Needed", finalHtml);
- }
-
- [Fact]
- public async Task ToggleSkipped_MarksItemSkipped()
- {
- var client = _factory.CreateClient();
- await WebTestHelpers.RegisterAsync(client, "skip1@example.com", "Skip User");
- await WebTestHelpers.CreateHouseholdAsync(client, "Skip Household");
- await AddItemAsync(client, "Bread");
-
- var listHtml = await client.GetStringAsync("/list");
- var (id, rowVersion) = ExtractFirstItemInfo(listHtml);
- var token = WebTestHelpers.ExtractAntiForgeryToken(listHtml);
-
- var response = await client.PostAsync($"/list/items/{id}/skip", new FormUrlEncodedContent(new Dictionary<string, string>
- {
- ["__RequestVerificationToken"] = token,
- ["rowVersion"] = rowVersion
- }));
- response.EnsureSuccessStatusCode();
-
- var updatedHtml = await client.GetStringAsync("/list");
- HtmlAssert.Contains("Skipped", updatedHtml);
- }
-
- [Fact]
- public async Task DeleteItem_RemovesItFromTheList()
- {
- var client = _factory.CreateClient();
- await WebTestHelpers.RegisterAsync(client, "delete1@example.com", "Delete User");
- await WebTestHelpers.CreateHouseholdAsync(client, "Delete Household");
- await AddItemAsync(client, "Eggs");
-
- var listHtml = await client.GetStringAsync("/list");
- var (id, rowVersion) = ExtractFirstItemInfo(listHtml);
- var token = WebTestHelpers.ExtractAntiForgeryToken(listHtml);
-
- var response = await client.PostAsync($"/list/items/{id}/delete", new FormUrlEncodedContent(new Dictionary<string, string>
- {
- ["__RequestVerificationToken"] = token,
- ["rowVersion"] = rowVersion
- }));
- response.EnsureSuccessStatusCode();
-
- var updatedHtml = await client.GetStringAsync("/list");
- Assert.DoesNotContain("Eggs", updatedHtml);
- HtmlAssert.Contains("No items yet", updatedHtml);
- }
-
- [Fact]
- public async Task ToggleItem_FailsForAnItemInAnotherHousehold()
- {
- var clientA = _factory.CreateClient();
- var clientB = _factory.CreateClient();
- await WebTestHelpers.RegisterAsync(clientA, "isoalice@example.com", "Iso Alice");
- await WebTestHelpers.RegisterAsync(clientB, "isobob@example.com", "Iso Bob");
- await WebTestHelpers.CreateHouseholdAsync(clientA, "Iso Household A");
- await WebTestHelpers.CreateHouseholdAsync(clientB, "Iso Household B");
- await AddItemAsync(clientA, "Alice Only Item");
-
- var listHtmlA = await clientA.GetStringAsync("/list");
- var (id, rowVersion) = ExtractFirstItemInfo(listHtmlA);
-
- var listHtmlB = await clientB.GetStringAsync("/list");
- var tokenB = WebTestHelpers.ExtractAntiForgeryToken(listHtmlB);
-
- var response = await clientB.PostAsync($"/list/items/{id}/toggle", new FormUrlEncodedContent(new Dictionary<string, string>
- {
- ["__RequestVerificationToken"] = tokenB,
- ["rowVersion"] = rowVersion
- }));
- response.EnsureSuccessStatusCode();
-
- var finalHtmlA = await clientA.GetStringAsync("/list");
- Assert.True(HasStatusBadge(finalHtmlA, "Needed"), "Expected the item to still show a Needed status badge.");
- Assert.False(HasStatusBadge(finalHtmlA, "Purchased"), "The other household's toggle should not have changed this item's status.");
- }
-
- [Fact]
- public async Task ToggleItem_AjaxRequestWithStaleRowVersionReturnsConflict()
- {
- var client = _factory.CreateClient();
- await WebTestHelpers.RegisterAsync(client, "conflict1@example.com", "Conflict User");
- await WebTestHelpers.CreateHouseholdAsync(client, "Conflict Household");
- await AddItemAsync(client, "Butter");
-
- var listHtml = await client.GetStringAsync("/list");
- var (id, _) = ExtractFirstItemInfo(listHtml);
- var token = WebTestHelpers.ExtractAntiForgeryToken(listHtml);
-
- var request = new HttpRequestMessage(HttpMethod.Post, $"/list/items/{id}/toggle")
- {
- Content = new FormUrlEncodedContent(new Dictionary<string, string>
- {
- ["__RequestVerificationToken"] = token,
- ["rowVersion"] = Guid.NewGuid().ToString()
- })
- };
- request.Headers.Add("X-Requested-With", "XMLHttpRequest");
-
- var response = await client.SendAsync(request);
-
- Assert.Equal(HttpStatusCode.Conflict, response.StatusCode);
- }
-
- private static async Task AddItemAsync(HttpClient client, string displayName)
- {
- var html = await client.GetStringAsync("/list");
- var token = WebTestHelpers.ExtractAntiForgeryToken(html);
-
- var response = await client.PostAsync("/list/items", new FormUrlEncodedContent(new Dictionary<string, string>
- {
- ["__RequestVerificationToken"] = token,
- ["NewItem.DisplayName"] = displayName
- }));
- response.EnsureSuccessStatusCode();
- }
-
- private static bool HasStatusBadge(string html, string status)
- {
- return Regex.IsMatch(html, $"badge bg-secondary\">{Regex.Escape(status)}</span>");
- }
-
- private static (string Id, string RowVersion) ExtractFirstItemInfo(string html)
- {
- var idMatch = Regex.Match(html, "id=\"list-item-([0-9a-fA-F-]{36})\"");
- if (!idMatch.Success)
- {
- throw new InvalidOperationException("No list item found in HTML.");
- }
-
- var window = html.Substring(idMatch.Index, Math.Min(1200, html.Length - idMatch.Index));
- var rowVersionMatch = Regex.Match(window, "name=\"rowVersion\" value=\"([^\"]+)\"");
- if (!rowVersionMatch.Success)
- {
- throw new InvalidOperationException("No rowVersion field found for the list item.");
- }
-
- return (idMatch.Groups[1].Value, rowVersionMatch.Groups[1].Value);
- }
- }
|