From 35bf8e806ff9f2141f128b99499f784d98f6e618 Mon Sep 17 00:00:00 2001 From: Dave Walker Date: Sun, 12 Nov 2023 11:02:19 +0000 Subject: [PATCH] Added a retailers controller --- .../Controllers/RetailersController.cs | 119 ++++++++++++++++++ .../Exceptions/RetailerInUseException.cs | 32 +++++ .../Interfaces/IRetailerManager.cs | 2 + .../Database/RetailerManager.cs | 65 ++++++++-- .../Factory/MusicCatalogueFactory.cs | 2 +- .../RetailerManagerTest.cs | 73 +++++++++-- 6 files changed, 275 insertions(+), 18 deletions(-) create mode 100644 src/MusicCatalogue.Api/Controllers/RetailersController.cs create mode 100644 src/MusicCatalogue.Entities/Exceptions/RetailerInUseException.cs diff --git a/src/MusicCatalogue.Api/Controllers/RetailersController.cs b/src/MusicCatalogue.Api/Controllers/RetailersController.cs new file mode 100644 index 0000000..90bf3a2 --- /dev/null +++ b/src/MusicCatalogue.Api/Controllers/RetailersController.cs @@ -0,0 +1,119 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using MusicCatalogue.Entities.Database; +using MusicCatalogue.Entities.Exceptions; +using MusicCatalogue.Entities.Interfaces; + +namespace MusicCatalogue.Api.Controllers +{ + [Authorize] + [ApiController] + [ApiConventionType(typeof(DefaultApiConventions))] + [Route("[controller]")] + public class RetailersController : Controller + { + private readonly IMusicCatalogueFactory _factory; + + public RetailersController(IMusicCatalogueFactory factory) + { + _factory = factory; + } + + /// + /// Return a list of all the retailers in the catalogue + /// + /// + [HttpGet] + [Route("")] + public async Task>> GetRetailersAsync() + { + // Get a list of all artists in the catalogue + List retailers = await _factory.Retailers.ListAsync(x => true); + + // If there are no artists, return a no content response + if (!retailers.Any()) + { + return NoContent(); + } + + return retailers; + } + + /// + /// Return retailer details given a retailer ID + /// + /// + /// + [HttpGet] + [Route("{id:int}")] + public async Task> GetRetailerByIdAsync(int id) + { + var retailer = await _factory.Retailers.GetAsync(x => x.Id == id); + + if (retailer == null) + { + return NotFound(); + } + + return retailer; + } + + /// + /// Add a retailer to the catalogue + /// + /// + /// + [HttpPost] + [Route("")] + public async Task> AddRetailerAsync([FromBody] Retailer template) + { + var retailer = await _factory.Retailers.AddAsync(template.Name); + return retailer; + } + + /// + /// Update an existing retailer + /// + /// + /// + [HttpPut] + [Route("")] + public async Task> UpdateRetailerAsync([FromBody] Retailer template) + { + var retailer = await _factory.Retailers.UpdateAsync(template.Id, template.Name); + return retailer; + } + + /// + /// Delete an existing retailer + /// + /// + /// + [HttpDelete] + [Route("{id}")] + public async Task DeleteRetailerAsync(int id) + { + // Make sure the retailer exists + var retailer = await _factory.Retailers.GetAsync(x => x.Id == id); + + // If the retailer doesn't exist, return a 404 + if (retailer == null) + { + return NotFound(); + } + + try + { + // Delete the retailer + await _factory.Retailers.DeleteAsync(id); + } + catch (RetailerInUseException) + { + // Retailer is in use so this is a bad request + return BadRequest(); + } + + return Ok(); + } + } +} \ No newline at end of file diff --git a/src/MusicCatalogue.Entities/Exceptions/RetailerInUseException.cs b/src/MusicCatalogue.Entities/Exceptions/RetailerInUseException.cs new file mode 100644 index 0000000..5ad1d0b --- /dev/null +++ b/src/MusicCatalogue.Entities/Exceptions/RetailerInUseException.cs @@ -0,0 +1,32 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.Serialization; + +namespace MusicCatalogue.Entities.Exceptions +{ + + [Serializable] + [ExcludeFromCodeCoverage] + public class RetailerInUseException : Exception + { + public RetailerInUseException() + { + } + + public RetailerInUseException(string message) : base(message) + { + } + + public RetailerInUseException(string message, Exception inner) : base(message, inner) + { + } + + protected RetailerInUseException(SerializationInfo serializationInfo, StreamingContext streamingContext) : base(serializationInfo, streamingContext) + { + } + + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + } + } +} \ No newline at end of file diff --git a/src/MusicCatalogue.Entities/Interfaces/IRetailerManager.cs b/src/MusicCatalogue.Entities/Interfaces/IRetailerManager.cs index a763672..6feda06 100644 --- a/src/MusicCatalogue.Entities/Interfaces/IRetailerManager.cs +++ b/src/MusicCatalogue.Entities/Interfaces/IRetailerManager.cs @@ -8,5 +8,7 @@ public interface IRetailerManager Task AddAsync(string name); Task GetAsync(Expression> predicate); Task> ListAsync(Expression> predicate); + Task UpdateAsync(int retailerId, string name); + Task DeleteAsync(int retailerId); } } \ No newline at end of file diff --git a/src/MusicCatalogue.Logic/Database/RetailerManager.cs b/src/MusicCatalogue.Logic/Database/RetailerManager.cs index 28caaca..19f3846 100644 --- a/src/MusicCatalogue.Logic/Database/RetailerManager.cs +++ b/src/MusicCatalogue.Logic/Database/RetailerManager.cs @@ -1,15 +1,22 @@ using Microsoft.EntityFrameworkCore; using MusicCatalogue.Data; using MusicCatalogue.Entities.Database; +using MusicCatalogue.Entities.Exceptions; using MusicCatalogue.Entities.Interfaces; +using MusicCatalogue.Logic.Factory; using System.Linq.Expressions; namespace MusicCatalogue.Logic.Database { - internal class RetailerManager : DatabaseManagerBase, IRetailerManager + internal class RetailerManager : IRetailerManager { - internal RetailerManager(MusicCatalogueDbContext context) : base(context) + private readonly MusicCatalogueFactory _factory; + private readonly MusicCatalogueDbContext? _context; + + internal RetailerManager(MusicCatalogueFactory factory) { + _factory = factory; + _context = factory.Context as MusicCatalogueDbContext; } /// @@ -32,10 +39,10 @@ public async Task GetAsync(Expression> predicate) /// /// public async Task> ListAsync(Expression> predicate) - => await _context.Retailers - .Where(predicate) - .OrderBy(x => x.Name) - .ToListAsync(); + => await _context!.Retailers + .Where(predicate) + .OrderBy(x => x.Name) + .ToListAsync(); /// /// Add a retailer, if they doesn't already exist @@ -53,11 +60,53 @@ public async Task AddAsync(string name) { Name = clean }; - await _context.Retailers.AddAsync(retailer); - await _context.SaveChangesAsync(); + await _context!.Retailers.AddAsync(retailer); + await _context!.SaveChangesAsync(); } return retailer; } + + /// + /// Update a retailer given their ID + /// + /// + /// + /// + public async Task UpdateAsync(int retailerId, string name) + { + var retailer = await GetAsync(a => a.Id == retailerId); + if (retailer != null) + { + retailer.Name = StringCleaner.Clean(name)!; + await _context!.SaveChangesAsync(); + } + return retailer; + } + + /// + /// Delete a retailer given their ID + /// + /// + /// + public async Task DeleteAsync(int retailerId) + { + // Check the retailer exists + var retailer = await GetAsync(a => a.Id == retailerId); + if (retailer != null) + { + // Check the retailer isn't in use + var albums = await _factory.Albums.ListAsync(x => x.RetailerId == retailerId); + if (albums.Any()) + { + var message = $"Retailer '{retailer.Name} with Id {retailerId} is in use and cannot be deleted"; + throw new RetailerInUseException(message); + } + + // Delete the retailer + _context!.Retailers.Remove(retailer); + await _context.SaveChangesAsync(); + } + } } } diff --git a/src/MusicCatalogue.Logic/Factory/MusicCatalogueFactory.cs b/src/MusicCatalogue.Logic/Factory/MusicCatalogueFactory.cs index c52c562..78f63de 100644 --- a/src/MusicCatalogue.Logic/Factory/MusicCatalogueFactory.cs +++ b/src/MusicCatalogue.Logic/Factory/MusicCatalogueFactory.cs @@ -44,7 +44,7 @@ public MusicCatalogueFactory(MusicCatalogueDbContext context) _artists = new Lazy(() => new ArtistManager(context)); _albums = new Lazy(() => new AlbumManager(this)); _tracks = new Lazy(() => new TrackManager(context)); - _retailers = new Lazy(() => new RetailerManager(context)); + _retailers = new Lazy(() => new RetailerManager(this)); _jobStatuses = new Lazy(() => new JobStatusManager(context)); _users = new Lazy(() => new UserManager(context)); _importer = new Lazy(() => new CsvImporter(this)); diff --git a/src/MusicCatalogue.Tests/RetailerManagerTest.cs b/src/MusicCatalogue.Tests/RetailerManagerTest.cs index e3f0cfb..f010ea1 100644 --- a/src/MusicCatalogue.Tests/RetailerManagerTest.cs +++ b/src/MusicCatalogue.Tests/RetailerManagerTest.cs @@ -1,4 +1,5 @@ using MusicCatalogue.Data; +using MusicCatalogue.Entities.Exceptions; using MusicCatalogue.Entities.Interfaces; using MusicCatalogue.Logic.Factory; @@ -7,30 +8,38 @@ namespace MusicCatalogue.Tests [TestClass] public class RetailerManagerTest { + private const string ArtistName = "John Coltrane"; + private const string AlbumTitle = "Blue Train"; + private const int Released = 1957; + private const string Genre = "Jazz"; + private const string CoverUrl = "https://some.host/blue-train.jpg"; + private const string TrackTitle = "Blue Train"; private const string Name = "Dig Vinyl"; + private const string UpdatedName = "Truck Store"; - private IRetailerManager? _manager = null; + private IMusicCatalogueFactory _factory = null; + private int _retailerId; [TestInitialize] public void TestInitialize() { MusicCatalogueDbContext context = MusicCatalogueDbContextFactory.CreateInMemoryDbContext(); - _manager = new MusicCatalogueFactory(context).Retailers; - Task.Run(() => _manager.AddAsync(Name)).Wait(); + _factory = new MusicCatalogueFactory(context); + _retailerId = Task.Run(() => _factory.Retailers.AddAsync(Name)).Result.Id; } [TestMethod] public async Task AddDuplicateTest() { - await _manager!.AddAsync(Name); - var retailers = await _manager.ListAsync(x => true); + await _factory!.Retailers.AddAsync(Name); + var retailers = await _factory.Retailers.ListAsync(x => true); Assert.AreEqual(1, retailers.Count); } [TestMethod] public async Task AddAndGetTest() { - var retailer = await _manager!.GetAsync(a => a.Name == Name); + var retailer = await _factory!.Retailers.GetAsync(a => a.Name == Name); Assert.IsNotNull(retailer); Assert.IsTrue(retailer.Id > 0); Assert.AreEqual(Name, retailer.Name); @@ -39,14 +48,14 @@ public async Task AddAndGetTest() [TestMethod] public async Task GetMissingTest() { - var retailer = await _manager!.GetAsync(a => a.Name == "Missing"); + var retailer = await _factory!.Retailers.GetAsync(a => a.Name == "Missing"); Assert.IsNull(retailer); } [TestMethod] public async Task ListAllTest() { - var retailers = await _manager!.ListAsync(x => true); + var retailers = await _factory!.Retailers.ListAsync(x => true); Assert.AreEqual(1, retailers!.Count); Assert.AreEqual(Name, retailers.First().Name); } @@ -54,8 +63,54 @@ public async Task ListAllTest() [TestMethod] public async Task ListMissingTest() { - var retailers = await _manager!.ListAsync(e => e.Name == "Missing"); + var retailers = await _factory!.Retailers.ListAsync(e => e.Name == "Missing"); Assert.AreEqual(0, retailers!.Count); } + + [TestMethod] + public async Task UpdateTest() + { + var retailer = await _factory!.Retailers.UpdateAsync(_retailerId, UpdatedName); + Assert.IsNotNull(retailer); + Assert.IsTrue(retailer.Id > 0); + Assert.AreEqual(UpdatedName, retailer.Name); + } + + [TestMethod] + public async Task UpdateMissingTest() + { + var retailer = await _factory!.Retailers.UpdateAsync(-1, UpdatedName); + Assert.IsNull(retailer); + } + + [TestMethod] + public async Task DeleteTest() + { + await _factory!.Retailers.DeleteAsync(_retailerId); + var retailers = await _factory!.Retailers.ListAsync(x => true); + Assert.IsNotNull(retailers); + Assert.IsFalse(retailers.Any()); + } + + [TestMethod] + public async Task DeleteMissingTest() + { + await _factory!.Retailers.DeleteAsync(-1); + var retailers = await _factory!.Retailers.ListAsync(x => true); + Assert.AreEqual(1, retailers!.Count); + Assert.AreEqual(Name, retailers.First().Name); + } + + [TestMethod] + [ExpectedException(typeof(RetailerInUseException))] + public async Task DeleteInUseTest() + { + // Add an album that uses the retailer + var artist = await _factory.Artists.AddAsync(ArtistName); + await _factory.Albums.AddAsync(artist.Id, AlbumTitle, Released, Genre, CoverUrl, false, null, null, _retailerId); + + // Now try to delete the retailer - this should raise an exception + await _factory!.Retailers.DeleteAsync(_retailerId); + } } }