25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

209 lines
8.3KB

  1. using System.Net;
  2. using System.Text.RegularExpressions;
  3. namespace CartWise.Web.Tests;
  4. public class ShoppingListItemActionsTests : IClassFixture<CartWiseWebApplicationFactory>
  5. {
  6. private readonly CartWiseWebApplicationFactory _factory;
  7. public ShoppingListItemActionsTests(CartWiseWebApplicationFactory factory)
  8. {
  9. _factory = factory;
  10. }
  11. [Fact]
  12. public async Task TogglePurchased_MarksItemPurchased()
  13. {
  14. var client = _factory.CreateClient();
  15. await WebTestHelpers.RegisterAsync(client, "toggle1@example.com", "Toggle User");
  16. await WebTestHelpers.CreateHouseholdAsync(client, "Toggle Household");
  17. await AddItemAsync(client, "Milk");
  18. var listHtml = await client.GetStringAsync("/list");
  19. var (id, rowVersion) = ExtractFirstItemInfo(listHtml);
  20. var token = WebTestHelpers.ExtractAntiForgeryToken(listHtml);
  21. var response = await client.PostAsync($"/list/items/{id}/toggle", new FormUrlEncodedContent(new Dictionary<string, string>
  22. {
  23. ["__RequestVerificationToken"] = token,
  24. ["rowVersion"] = rowVersion
  25. }));
  26. response.EnsureSuccessStatusCode();
  27. var updatedHtml = await client.GetStringAsync("/list");
  28. HtmlAssert.Contains("Purchased", updatedHtml);
  29. }
  30. [Fact]
  31. public async Task TogglePurchased_TwiceReturnsItemToNeeded()
  32. {
  33. var client = _factory.CreateClient();
  34. await WebTestHelpers.RegisterAsync(client, "toggle2@example.com", "Toggle User 2");
  35. await WebTestHelpers.CreateHouseholdAsync(client, "Toggle Household 2");
  36. await AddItemAsync(client, "Milk");
  37. var listHtml = await client.GetStringAsync("/list");
  38. var (id, rowVersion) = ExtractFirstItemInfo(listHtml);
  39. var token = WebTestHelpers.ExtractAntiForgeryToken(listHtml);
  40. await client.PostAsync($"/list/items/{id}/toggle", new FormUrlEncodedContent(new Dictionary<string, string>
  41. {
  42. ["__RequestVerificationToken"] = token,
  43. ["rowVersion"] = rowVersion
  44. }));
  45. var midHtml = await client.GetStringAsync("/list");
  46. var (_, secondRowVersion) = ExtractFirstItemInfo(midHtml);
  47. var midToken = WebTestHelpers.ExtractAntiForgeryToken(midHtml);
  48. await client.PostAsync($"/list/items/{id}/toggle", new FormUrlEncodedContent(new Dictionary<string, string>
  49. {
  50. ["__RequestVerificationToken"] = midToken,
  51. ["rowVersion"] = secondRowVersion
  52. }));
  53. var finalHtml = await client.GetStringAsync("/list");
  54. HtmlAssert.Contains("Needed", finalHtml);
  55. }
  56. [Fact]
  57. public async Task ToggleSkipped_MarksItemSkipped()
  58. {
  59. var client = _factory.CreateClient();
  60. await WebTestHelpers.RegisterAsync(client, "skip1@example.com", "Skip User");
  61. await WebTestHelpers.CreateHouseholdAsync(client, "Skip Household");
  62. await AddItemAsync(client, "Bread");
  63. var listHtml = await client.GetStringAsync("/list");
  64. var (id, rowVersion) = ExtractFirstItemInfo(listHtml);
  65. var token = WebTestHelpers.ExtractAntiForgeryToken(listHtml);
  66. var response = await client.PostAsync($"/list/items/{id}/skip", new FormUrlEncodedContent(new Dictionary<string, string>
  67. {
  68. ["__RequestVerificationToken"] = token,
  69. ["rowVersion"] = rowVersion
  70. }));
  71. response.EnsureSuccessStatusCode();
  72. var updatedHtml = await client.GetStringAsync("/list");
  73. HtmlAssert.Contains("Skipped", updatedHtml);
  74. }
  75. [Fact]
  76. public async Task DeleteItem_RemovesItFromTheList()
  77. {
  78. var client = _factory.CreateClient();
  79. await WebTestHelpers.RegisterAsync(client, "delete1@example.com", "Delete User");
  80. await WebTestHelpers.CreateHouseholdAsync(client, "Delete Household");
  81. await AddItemAsync(client, "Eggs");
  82. var listHtml = await client.GetStringAsync("/list");
  83. var (id, rowVersion) = ExtractFirstItemInfo(listHtml);
  84. var token = WebTestHelpers.ExtractAntiForgeryToken(listHtml);
  85. var response = await client.PostAsync($"/list/items/{id}/delete", new FormUrlEncodedContent(new Dictionary<string, string>
  86. {
  87. ["__RequestVerificationToken"] = token,
  88. ["rowVersion"] = rowVersion
  89. }));
  90. response.EnsureSuccessStatusCode();
  91. var updatedHtml = await client.GetStringAsync("/list");
  92. Assert.DoesNotContain("Eggs", updatedHtml);
  93. HtmlAssert.Contains("No items yet", updatedHtml);
  94. }
  95. [Fact]
  96. public async Task ToggleItem_FailsForAnItemInAnotherHousehold()
  97. {
  98. var clientA = _factory.CreateClient();
  99. var clientB = _factory.CreateClient();
  100. await WebTestHelpers.RegisterAsync(clientA, "isoalice@example.com", "Iso Alice");
  101. await WebTestHelpers.RegisterAsync(clientB, "isobob@example.com", "Iso Bob");
  102. await WebTestHelpers.CreateHouseholdAsync(clientA, "Iso Household A");
  103. await WebTestHelpers.CreateHouseholdAsync(clientB, "Iso Household B");
  104. await AddItemAsync(clientA, "Alice Only Item");
  105. var listHtmlA = await clientA.GetStringAsync("/list");
  106. var (id, rowVersion) = ExtractFirstItemInfo(listHtmlA);
  107. var listHtmlB = await clientB.GetStringAsync("/list");
  108. var tokenB = WebTestHelpers.ExtractAntiForgeryToken(listHtmlB);
  109. var response = await clientB.PostAsync($"/list/items/{id}/toggle", new FormUrlEncodedContent(new Dictionary<string, string>
  110. {
  111. ["__RequestVerificationToken"] = tokenB,
  112. ["rowVersion"] = rowVersion
  113. }));
  114. response.EnsureSuccessStatusCode();
  115. var finalHtmlA = await clientA.GetStringAsync("/list");
  116. Assert.True(HasStatusBadge(finalHtmlA, "Needed"), "Expected the item to still show a Needed status badge.");
  117. Assert.False(HasStatusBadge(finalHtmlA, "Purchased"), "The other household's toggle should not have changed this item's status.");
  118. }
  119. [Fact]
  120. public async Task ToggleItem_AjaxRequestWithStaleRowVersionReturnsConflict()
  121. {
  122. var client = _factory.CreateClient();
  123. await WebTestHelpers.RegisterAsync(client, "conflict1@example.com", "Conflict User");
  124. await WebTestHelpers.CreateHouseholdAsync(client, "Conflict Household");
  125. await AddItemAsync(client, "Butter");
  126. var listHtml = await client.GetStringAsync("/list");
  127. var (id, _) = ExtractFirstItemInfo(listHtml);
  128. var token = WebTestHelpers.ExtractAntiForgeryToken(listHtml);
  129. var request = new HttpRequestMessage(HttpMethod.Post, $"/list/items/{id}/toggle")
  130. {
  131. Content = new FormUrlEncodedContent(new Dictionary<string, string>
  132. {
  133. ["__RequestVerificationToken"] = token,
  134. ["rowVersion"] = Guid.NewGuid().ToString()
  135. })
  136. };
  137. request.Headers.Add("X-Requested-With", "XMLHttpRequest");
  138. var response = await client.SendAsync(request);
  139. Assert.Equal(HttpStatusCode.Conflict, response.StatusCode);
  140. }
  141. private static async Task AddItemAsync(HttpClient client, string displayName)
  142. {
  143. var html = await client.GetStringAsync("/list");
  144. var token = WebTestHelpers.ExtractAntiForgeryToken(html);
  145. var response = await client.PostAsync("/list/items", new FormUrlEncodedContent(new Dictionary<string, string>
  146. {
  147. ["__RequestVerificationToken"] = token,
  148. ["NewItem.DisplayName"] = displayName
  149. }));
  150. response.EnsureSuccessStatusCode();
  151. }
  152. private static bool HasStatusBadge(string html, string status)
  153. {
  154. return Regex.IsMatch(html, $"badge bg-secondary\">{Regex.Escape(status)}</span>");
  155. }
  156. private static (string Id, string RowVersion) ExtractFirstItemInfo(string html)
  157. {
  158. var idMatch = Regex.Match(html, "id=\"list-item-([0-9a-fA-F-]{36})\"");
  159. if (!idMatch.Success)
  160. {
  161. throw new InvalidOperationException("No list item found in HTML.");
  162. }
  163. var window = html.Substring(idMatch.Index, Math.Min(1200, html.Length - idMatch.Index));
  164. var rowVersionMatch = Regex.Match(window, "name=\"rowVersion\" value=\"([^\"]+)\"");
  165. if (!rowVersionMatch.Success)
  166. {
  167. throw new InvalidOperationException("No rowVersion field found for the list item.");
  168. }
  169. return (idMatch.Groups[1].Value, rowVersionMatch.Groups[1].Value);
  170. }
  171. }

Powered by TurnKey Linux.