From ee7d26cc8656d600da6da8b5fa92fe0c9cb1854f Mon Sep 17 00:00:00 2001 From: A-Guldborg <95026056+A-Guldborg@users.noreply.github.com> Date: Tue, 12 Dec 2023 11:27:33 +0100 Subject: [PATCH] Adds GET products/all for privileged users (#237) The API adds the endpoint GET /api/v2/products/all, which returns all products, regardless of usergroup or visibility. This endpoint will be used for the product management frontend in https://github.com/AnalogIO/shifty-webapp/pull/20, discussed in #217 (and slack). This endpoint is only accessible for users with Board privileges and will allow board members to manage the visibilty of products or products that they otherwise don't have access to themselves in the app. --------- Co-authored-by: Omid Marfavi <21163286+marfavi@users.noreply.github.com> --- .../Services/v2/IProductService.cs | 1 + .../Services/v2/ProductService.cs | 8 + .../v2/Products/ProductResponse.cs | 9 ++ .../Services/v2/ProductServiceTest.cs | 145 ++++++++++++++++-- .../Controllers/v2/ProductsController.cs | 15 ++ 5 files changed, 163 insertions(+), 15 deletions(-) diff --git a/coffeecard/CoffeeCard.Library/Services/v2/IProductService.cs b/coffeecard/CoffeeCard.Library/Services/v2/IProductService.cs index 4249bdbb..45607541 100644 --- a/coffeecard/CoffeeCard.Library/Services/v2/IProductService.cs +++ b/coffeecard/CoffeeCard.Library/Services/v2/IProductService.cs @@ -10,6 +10,7 @@ public interface IProductService : IDisposable { Task> GetProductsForUserAsync(User user); Task GetProductAsync(int productId); + Task> GetAllProductsAsync(); Task AddProduct(AddProductRequest product); Task UpdateProduct(UpdateProductRequest product); diff --git a/coffeecard/CoffeeCard.Library/Services/v2/ProductService.cs b/coffeecard/CoffeeCard.Library/Services/v2/ProductService.cs index f03e88ad..3d440313 100644 --- a/coffeecard/CoffeeCard.Library/Services/v2/ProductService.cs +++ b/coffeecard/CoffeeCard.Library/Services/v2/ProductService.cs @@ -35,6 +35,14 @@ private async Task> GetProductsAsync(UserGroup userGroup) .ToListAsync(); } + public async Task> GetAllProductsAsync() + { + return await _context.Products + .OrderBy(p => p.Id) + .Include(p => p.ProductUserGroup) + .ToListAsync(); + } + public async Task GetProductAsync(int productId) { var product = await _context.Products diff --git a/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Products/ProductResponse.cs b/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Products/ProductResponse.cs index 959cd2b0..987a1ca4 100644 --- a/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Products/ProductResponse.cs +++ b/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Products/ProductResponse.cs @@ -15,6 +15,7 @@ namespace CoffeeCard.Models.DataTransferObjects.v2.Products /// "name": "Coffee clip card", /// "description": "Coffee clip card of 10 clips", /// "isPerk": true, + /// "visible": true, /// "AllowedUserGroups": ["Manager", "Board"] /// } /// @@ -68,6 +69,14 @@ public class ProductResponse [Required] public bool IsPerk { get; set; } + /// + /// Visibility of products for users + /// + /// Product visibility + /// true + [Required] + public bool Visible { get; set; } + /// /// Decides the user groups that can access the product. /// diff --git a/coffeecard/CoffeeCard.Tests.Unit/Services/v2/ProductServiceTest.cs b/coffeecard/CoffeeCard.Tests.Unit/Services/v2/ProductServiceTest.cs index f50876ed..85c0a83d 100644 --- a/coffeecard/CoffeeCard.Tests.Unit/Services/v2/ProductServiceTest.cs +++ b/coffeecard/CoffeeCard.Tests.Unit/Services/v2/ProductServiceTest.cs @@ -1,5 +1,5 @@ +using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Linq; using System.Threading.Tasks; using CoffeeCard.Common.Configuration; @@ -70,16 +70,15 @@ await productService.UpdateProduct(new UpdateProductRequest() AllowedUserGroups = new List() { UserGroup.Customer, UserGroup.Board } }); - var expected = new List - { - UserGroup.Customer, UserGroup.Board - }; - var result = await productService.GetProductAsync(1); - Assert.Collection(expected, - e => e.Equals(UserGroup.Customer), - e => e.Equals(UserGroup.Board)); + Assert.Collection(result.ProductUserGroup, + e => Assert.Equal(UserGroup.Customer, e.UserGroup), + e => Assert.Equal(UserGroup.Board, e.UserGroup)); + + // Explicitly check for exclusion of Barista and Manager, even though Assert.Collection implicitly covers it. + Assert.DoesNotContain(UserGroup.Barista, result.ProductUserGroup.Select(e => e.UserGroup)); + Assert.DoesNotContain(UserGroup.Manager, result.ProductUserGroup.Select(e => e.UserGroup)); } [Fact(DisplayName = "AddProduct adds only selected user groups")] @@ -113,16 +112,132 @@ public async Task AddProduct_Sets_Correct_UserGroups() await productService.AddProduct(p); - var expected = new List + var result = await productService.GetProductAsync(1); + + Assert.Collection(result.ProductUserGroup, + e => Assert.Equal(UserGroup.Manager, e.UserGroup), + e => Assert.Equal(UserGroup.Board, e.UserGroup)); + } + + [Fact(DisplayName = "GetAllProducts shows non-visible products")] + public async Task GetAllProducts_Returns_Non_Visible_Products() + { + var builder = new DbContextOptionsBuilder() + .UseInMemoryDatabase(nameof(GetAllProducts_Returns_Non_Visible_Products)); + + var databaseSettings = new DatabaseSettings { - UserGroup.Manager, UserGroup.Board + SchemaName = "test" + }; + var environmentSettings = new EnvironmentSettings() + { + EnvironmentType = EnvironmentType.Test }; - var result = await productService.GetProductAsync(1); + await using var context = new CoffeeCardContext(builder.Options, databaseSettings, environmentSettings); + + using var productService = new ProductService(context); + + var p1 = new AddProductRequest + { + Name = "Coffee", + Description = "Coffee Clip card", + NumberOfTickets = 10, + Price = 10, + Visible = true, + AllowedUserGroups = Enum.GetValues() + }; + await productService.AddProduct(p1); + + var p2 = new AddProductRequest + { + Name = "Latte", + Description = "Fancy Drink Clip card", + NumberOfTickets = 10, + Price = 170, + Visible = false, + AllowedUserGroups = Enum.GetValues() + }; + await productService.AddProduct(p2); + + var result = await productService.GetAllProductsAsync(); + + Assert.Collection(result, + e => e.Visible.Equals(true), + e => e.Visible.Equals(false)); + } + + [Fact(DisplayName = "GetAllProducts returns products from all user groups")] + public async Task GetAllProducts_Returns_Products_For_All_UserGroups() + { + var builder = new DbContextOptionsBuilder() + .UseInMemoryDatabase(nameof(GetAllProducts_Returns_Products_For_All_UserGroups)); + + var databaseSettings = new DatabaseSettings + { + SchemaName = "test" + }; + var environmentSettings = new EnvironmentSettings() + { + EnvironmentType = EnvironmentType.Test + }; + + await using var context = new CoffeeCardContext(builder.Options, databaseSettings, environmentSettings); + + using var productService = new ProductService(context); + + var p1 = new AddProductRequest + { + Name = "Coffee", + Description = "Coffee Clip card", + NumberOfTickets = 10, + Price = 10, + Visible = true, + AllowedUserGroups = new List { UserGroup.Customer } + }; + await productService.AddProduct(p1); + + var p2 = new AddProductRequest + { + Name = "Latte", + Description = "Fancy Drink Clip card", + NumberOfTickets = 10, + Price = 170, + Visible = true, + AllowedUserGroups = new List { UserGroup.Barista } + }; + await productService.AddProduct(p2); + + var p3 = new AddProductRequest + { + Name = "Frappuccino", + Description = "Blended ice with sugar", + NumberOfTickets = 1, + Price = 35, + Visible = true, + AllowedUserGroups = new List { UserGroup.Manager } + }; + await productService.AddProduct(p3); + + var p4 = new AddProductRequest + { + Name = "Cortado", + Description = "Some spanish coffee", + NumberOfTickets = 1, + Price = 19, + Visible = true, + AllowedUserGroups = new List { UserGroup.Board } + }; + await productService.AddProduct(p4); + + var result = await productService.GetAllProductsAsync(); - Assert.Collection(expected, - e => e.Equals(UserGroup.Customer), - e => e.Equals(UserGroup.Board)); + Assert.Collection(result, + e => e.ProductUserGroup = new List { new ProductUserGroup { UserGroup = UserGroup.Customer } }, + e => e.ProductUserGroup = new List { new ProductUserGroup { UserGroup = UserGroup.Barista } }, + e => e.ProductUserGroup = new List { new ProductUserGroup { UserGroup = UserGroup.Manager } }, + e => e.ProductUserGroup = new List { new ProductUserGroup { UserGroup = UserGroup.Board } } + ); } } } \ No newline at end of file diff --git a/coffeecard/CoffeeCard.WebApi/Controllers/v2/ProductsController.cs b/coffeecard/CoffeeCard.WebApi/Controllers/v2/ProductsController.cs index c8c8f7c5..e8f95ee0 100644 --- a/coffeecard/CoffeeCard.WebApi/Controllers/v2/ProductsController.cs +++ b/coffeecard/CoffeeCard.WebApi/Controllers/v2/ProductsController.cs @@ -91,8 +91,23 @@ private static ProductResponse MapProductToDto(Product product) NumberOfTickets = product.NumberOfTickets, Price = product.Price, IsPerk = product.IsPerk(), + Visible = product.Visible, AllowedUserGroups = product.ProductUserGroup.Select(e => e.UserGroup) }; } + + /// + /// Returns a list of all products + /// + /// List of all products + /// Successful request + [HttpGet("all")] + [AuthorizeRoles(UserGroup.Board)] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task>> GetAllProducts() + { + IEnumerable products = await _productService.GetAllProductsAsync(); + return Ok(products.Select(MapProductToDto).ToList()); + } } }