diff --git a/src/ShippingRecorder.Api/Controllers/ExportController.cs b/src/ShippingRecorder.Api/Controllers/ExportController.cs index 47ca90a..b69cbf1 100644 --- a/src/ShippingRecorder.Api/Controllers/ExportController.cs +++ b/src/ShippingRecorder.Api/Controllers/ExportController.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Mvc; using ShippingRecorder.Api.Interfaces; using ShippingRecorder.Api.Entities; +using ShippingRecorder.Entities.Jobs; namespace HealthTracker.Api.Controllers { @@ -14,35 +15,55 @@ public class ExportController : Controller private readonly IBackgroundQueue _countryQueue; private readonly IBackgroundQueue _locationQueue; private readonly IBackgroundQueue _operatorQueue; + private readonly IBackgroundQueue _portQueue; + private readonly IBackgroundQueue _sightingQueue; private readonly IBackgroundQueue _vesselQueue; private readonly IBackgroundQueue _vesselTypeQueue; private readonly IBackgroundQueue _voyageQueue; - private readonly IBackgroundQueue _sightingQueue; - private readonly IBackgroundQueue _portQueue; public ExportController( IBackgroundQueue countryQueue, IBackgroundQueue locationQueue, IBackgroundQueue operatorQueue, + IBackgroundQueue portQueue, + IBackgroundQueue sightingQueue, IBackgroundQueue vesselQueue, IBackgroundQueue vesselTypeQueue, - IBackgroundQueue voyageQueue, - IBackgroundQueue sightingQueue, - IBackgroundQueue portQueue) + IBackgroundQueue voyageQueue) { _countryQueue = countryQueue; _locationQueue = locationQueue; _operatorQueue = operatorQueue; + _portQueue = portQueue; + _sightingQueue = sightingQueue; _vesselQueue = vesselQueue; _vesselTypeQueue = vesselTypeQueue; _voyageQueue = voyageQueue; - _sightingQueue = sightingQueue; - _portQueue = portQueue; + } + + [HttpPost] + [Route("all")] + public IActionResult ExportAll([FromBody] AllExportWorkItem item) + { + // Get the file name without the extension or path + var fileName = Path.GetFileNameWithoutExtension(item.FileName); + + // Queue exports for each data type + _countryQueue.Enqueue(BuildWorkItem("Countries", fileName)); + _locationQueue.Enqueue(BuildWorkItem("Locations", fileName)); + _operatorQueue.Enqueue(BuildWorkItem("Operators", fileName)); + _portQueue.Enqueue(BuildWorkItem("Ports", fileName)); + _sightingQueue.Enqueue(BuildWorkItem("Sightings", fileName)); + _vesselQueue.Enqueue(BuildWorkItem("Vessels", fileName)); + _vesselTypeQueue.Enqueue(BuildWorkItem("Vessel-Types", fileName)); + _voyageQueue.Enqueue(BuildWorkItem("Voyages", fileName)); + + return Accepted(); } [HttpPost] [Route("countries")] - public IActionResult ExportCountris([FromBody] CountryExportWorkItem item) + public IActionResult ExportCountries([FromBody] CountryExportWorkItem item) { item.JobName = "Country Export"; _countryQueue.Enqueue(item); @@ -67,6 +88,24 @@ public IActionResult ExportOperators([FromBody] OperatorExportWorkItem item) return Accepted(); } + [HttpPost] + [Route("ports")] + public IActionResult ExportPorts([FromBody] PortExportWorkItem item) + { + item.JobName = "Port Export"; + _portQueue.Enqueue(item); + return Accepted(); + } + + [HttpPost] + [Route("sightings")] + public IActionResult ExportSightings([FromBody] SightingExportWorkItem item) + { + item.JobName = "Sighting Export"; + _sightingQueue.Enqueue(item); + return Accepted(); + } + [HttpPost] [Route("vessels")] public IActionResult ExportVessels([FromBody] VesselExportWorkItem item) @@ -85,15 +124,6 @@ public IActionResult ExportVesselTypes([FromBody] VesselTypeExportWorkItem item) return Accepted(); } - [HttpPost] - [Route("sightings")] - public IActionResult ExportSightings([FromBody] SightingExportWorkItem item) - { - item.JobName = "Sighting Export"; - _sightingQueue.Enqueue(item); - return Accepted(); - } - [HttpPost] [Route("voyages")] public IActionResult ExportVoyages([FromBody] VoyageExportWorkItem item) @@ -103,13 +133,17 @@ public IActionResult ExportVoyages([FromBody] VoyageExportWorkItem item) return Accepted(); } - [HttpPost] - [Route("ports")] - public IActionResult ExportPorts([FromBody] PortExportWorkItem item) - { - item.JobName = "Port Export"; - _portQueue.Enqueue(item); - return Accepted(); - } + /// + /// Helper method to build a work item for the specified entity type + /// + /// + /// + /// + private T BuildWorkItem(string entityName, string fileName) where T : ExportWorkItem, new() + => new() + { + JobName = $"{entityName} Export", + FileName = $"{fileName}-{entityName}.csv" + }; } } diff --git a/src/ShippingRecorder.Api/Controllers/ImportController.cs b/src/ShippingRecorder.Api/Controllers/ImportController.cs index 3a6911d..3c5dc33 100644 --- a/src/ShippingRecorder.Api/Controllers/ImportController.cs +++ b/src/ShippingRecorder.Api/Controllers/ImportController.cs @@ -76,6 +76,15 @@ public IActionResult ImportPorts([FromBody] PortImportWorkItem item) return Accepted(); } + [HttpPost] + [Route("sightings")] + public IActionResult ImportSightings([FromBody] SightingImportWorkItem item) + { + item.JobName = "Sighting Import"; + _sightingQueue.Enqueue(item); + return Accepted(); + } + [HttpPost] [Route("vessels")] public IActionResult ImportVessels([FromBody] VesselImportWorkItem item) @@ -94,15 +103,6 @@ public IActionResult ImportVesselTypes([FromBody] VesselTypeImportWorkItem item) return Accepted(); } - [HttpPost] - [Route("sightings")] - public IActionResult ImportSightings([FromBody] SightingImportWorkItem item) - { - item.JobName = "Sighting Import"; - _sightingQueue.Enqueue(item); - return Accepted(); - } - [HttpPost] [Route("voyages")] public IActionResult ImportVoyages([FromBody] VoyageImportWorkItem item) diff --git a/src/ShippingRecorder.Api/Entities/AllExportWorkItem.cs b/src/ShippingRecorder.Api/Entities/AllExportWorkItem.cs new file mode 100644 index 0000000..7e34571 --- /dev/null +++ b/src/ShippingRecorder.Api/Entities/AllExportWorkItem.cs @@ -0,0 +1,8 @@ +using ShippingRecorder.Entities.Jobs; + +namespace ShippingRecorder.Api.Entities +{ + public class AllExportWorkItem : ExportWorkItem + { + } +} diff --git a/src/ShippingRecorder.Client/ApiClient/ExportClient.cs b/src/ShippingRecorder.Client/ApiClient/ExportClient.cs new file mode 100644 index 0000000..599e7d8 --- /dev/null +++ b/src/ShippingRecorder.Client/ApiClient/ExportClient.cs @@ -0,0 +1,41 @@ +using ShippingRecorder.Client.Interfaces; +using ShippingRecorder.Entities.Interfaces; +using Microsoft.Extensions.Logging; +using System.Threading.Tasks; +using ShippingRecorder.Entities.Db; +using System.Net.Http; +using System.Linq; +using System.Collections.Generic; +using System.IO; +using System; + +namespace ShippingRecorder.Client.ApiClient +{ + public class ExportClient : ShippingRecorderClientBase, IExportClient + { + private const string RouteKey = "Export"; + + public ExportClient( + IShippingRecorderHttpClient client, + IShippingRecorderApplicationSettings settings, + IAuthenticationTokenProvider tokenProvider, + ICacheWrapper cache, + ILogger logger) + : base(client, settings, tokenProvider, cache, logger) + { + } + + /// + /// Request an export of allexports to a named file in the export allexport + /// + /// + /// + /// + public async Task ExportAsync(string fileName) + { + dynamic data = new{ FileName = fileName }; + var json = Serialize(data); + await SendIndirectAsync(RouteKey, json, HttpMethod.Post); + } + } +} diff --git a/src/ShippingRecorder.Client/Interfaces/IExportClient.cs b/src/ShippingRecorder.Client/Interfaces/IExportClient.cs new file mode 100644 index 0000000..3ce7b47 --- /dev/null +++ b/src/ShippingRecorder.Client/Interfaces/IExportClient.cs @@ -0,0 +1,6 @@ +namespace ShippingRecorder.Client.Interfaces +{ + public interface IExportClient : IExporter + { + } +} \ No newline at end of file diff --git a/src/ShippingRecorder.Mvc/Controllers/DataExchangeControllerBase.cs b/src/ShippingRecorder.Mvc/Controllers/DataExchangeControllerBase.cs index f8e7e26..17cb6c9 100644 --- a/src/ShippingRecorder.Mvc/Controllers/DataExchangeControllerBase.cs +++ b/src/ShippingRecorder.Mvc/Controllers/DataExchangeControllerBase.cs @@ -23,6 +23,7 @@ public abstract class DataExchangeControllerBase : ShippingRecorderControllerBas public DataExchangeControllerBase( ICountryClient countryClient, + IExportClient exportClient, ILocationClient locationClient, IOperatorClient operatorClient, IPortClient portClient, @@ -42,6 +43,7 @@ public DataExchangeControllerBase( _importers.Add(DataExchangeType.VesselTypes, vesselTypeClient); _importers.Add(DataExchangeType.Voyages, voyageClient); + _exporters.Add(DataExchangeType.All, exportClient); _exporters.Add(DataExchangeType.Countries, countryClient); _exporters.Add(DataExchangeType.Locations, locationClient); _exporters.Add(DataExchangeType.Operators, operatorClient); diff --git a/src/ShippingRecorder.Mvc/Controllers/ExportController.cs b/src/ShippingRecorder.Mvc/Controllers/ExportController.cs index cac041b..f88d463 100644 --- a/src/ShippingRecorder.Mvc/Controllers/ExportController.cs +++ b/src/ShippingRecorder.Mvc/Controllers/ExportController.cs @@ -12,6 +12,7 @@ public class ExportController : DataExchangeControllerBase { public ExportController( ICountryClient countryClient, + IExportClient exportClient, ILocationClient locationClient, IOperatorClient operatorClient, IPortClient portClient, @@ -22,6 +23,7 @@ public ExportController( IPartialViewToStringRenderer renderer, ILogger logger) : base( countryClient, + exportClient, locationClient, operatorClient, portClient, diff --git a/src/ShippingRecorder.Mvc/Controllers/ImportController.cs b/src/ShippingRecorder.Mvc/Controllers/ImportController.cs index 0cb9609..ea0eaba 100644 --- a/src/ShippingRecorder.Mvc/Controllers/ImportController.cs +++ b/src/ShippingRecorder.Mvc/Controllers/ImportController.cs @@ -15,6 +15,7 @@ public class ImportController : DataExchangeControllerBase public ImportController( ICountryClient countryClient, + IExportClient exportClient, ILocationClient locationClient, IOperatorClient operatorClient, IPortClient portClient, @@ -25,6 +26,7 @@ public ImportController( IPartialViewToStringRenderer renderer, ILogger logger) : base( countryClient, + exportClient, locationClient, operatorClient, portClient, diff --git a/src/ShippingRecorder.Mvc/Entities/DataExchangeType.cs b/src/ShippingRecorder.Mvc/Entities/DataExchangeType.cs index 7da92ea..503f401 100644 --- a/src/ShippingRecorder.Mvc/Entities/DataExchangeType.cs +++ b/src/ShippingRecorder.Mvc/Entities/DataExchangeType.cs @@ -3,6 +3,7 @@ namespace ShippingRecorder.Mvc.Enumerations public enum DataExchangeType { None, + All, Countries, Locations, Operators, diff --git a/src/ShippingRecorder.Mvc/Helpers/DataExchangeTypeExtensions.cs b/src/ShippingRecorder.Mvc/Helpers/DataExchangeTypeExtensions.cs index a3be9fa..9e96ce1 100644 --- a/src/ShippingRecorder.Mvc/Helpers/DataExchangeTypeExtensions.cs +++ b/src/ShippingRecorder.Mvc/Helpers/DataExchangeTypeExtensions.cs @@ -14,6 +14,7 @@ public static string ToName(this DataExchangeType type) return type switch { DataExchangeType.None => "", + DataExchangeType.All => "All Data", DataExchangeType.Countries => "Countries", DataExchangeType.Locations => "Locations", DataExchangeType.Operators => "Operators", diff --git a/src/ShippingRecorder.Mvc/Models/ImportViewModel.cs b/src/ShippingRecorder.Mvc/Models/ImportViewModel.cs index 830d899..e4e5add 100644 --- a/src/ShippingRecorder.Mvc/Models/ImportViewModel.cs +++ b/src/ShippingRecorder.Mvc/Models/ImportViewModel.cs @@ -18,7 +18,7 @@ public class ImportViewModel : DataExchangeViewModel public ImportViewModel() { - foreach (var importType in Enum.GetValues()) + foreach (var importType in Enum.GetValues().Where(x => x != DataExchangeType.All)) { var importTypeName = importType.ToName(); ImportTypes.Add(new SelectListItem() { Text = $"{importTypeName}", Value = importType.ToString() }); diff --git a/src/ShippingRecorder.Mvc/Startup.cs b/src/ShippingRecorder.Mvc/Startup.cs index 078b1fb..c29fee4 100644 --- a/src/ShippingRecorder.Mvc/Startup.cs +++ b/src/ShippingRecorder.Mvc/Startup.cs @@ -86,6 +86,7 @@ public void ConfigureServices(IServiceCollection services) services.AddSingleton(provider => ShippingRecorderHttpClient.Instance); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/src/ShippingRecorder.Mvc/appsettings.json b/src/ShippingRecorder.Mvc/appsettings.json index cee0e18..98ebd2f 100644 --- a/src/ShippingRecorder.Mvc/appsettings.json +++ b/src/ShippingRecorder.Mvc/appsettings.json @@ -146,6 +146,10 @@ { "Name": "ExportPort", "Route": "/export/ports" + }, + { + "Name": "Export", + "Route": "/export/all" } ], "UseCustomErrorPageInDevelopment": true, diff --git a/src/ShippingRecorder.Tests/Client/ExportClientTest.cs b/src/ShippingRecorder.Tests/Client/ExportClientTest.cs new file mode 100644 index 0000000..df0a8cb --- /dev/null +++ b/src/ShippingRecorder.Tests/Client/ExportClientTest.cs @@ -0,0 +1,70 @@ +using System.Text.Json; +using ShippingRecorder.Client.Interfaces; +using ShippingRecorder.Tests.Mocks; +using Microsoft.Extensions.Logging; +using Moq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using ShippingRecorder.Entities.Config; +using ShippingRecorder.Client.ApiClient; +using System.Threading.Tasks; +using System.Net.Http; +using System.Collections.Generic; +using System.Linq; +using System.IO; + +namespace ShippingRecorder.Tests.Client +{ + [TestClass] + public class ExportClientTest + { + private readonly string ApiToken = "An API Token"; + private readonly MockShippingRecorderHttpClient _httpClient = new(); + private IExportClient _client; + private string _filePath; + + private readonly ShippingRecorderApplicationSettings _settings = new() + { + ApiUrl = "http://server/", + ApiRoutes = [ + new() { Name = "Export", Route = "/export/all" } + ] + }; + + [TestInitialize] + public void Initialise() + { + var provider = new Mock(); + provider.Setup(x => x.GetToken()).Returns(ApiToken); + var logger = new Mock>(); + var cache = new Mock(); + _client = new ExportClient(_httpClient, _settings, provider.Object, cache.Object, logger.Object); + } + + [TestCleanup] + public void CleanUp() + { + if (!string.IsNullOrEmpty(_filePath) && File.Exists(_filePath)) + { + File.Delete(_filePath); + } + } + + [TestMethod] + public async Task ExportTest() + { + _filePath = Path.ChangeExtension(Path.GetTempFileName(), "csv"); + _httpClient.AddResponse(""); + + var json = JsonSerializer.Serialize(new { FileName = _filePath }); + var expectedRoute = _settings.ApiRoutes.First(x => x.Name == "Export").Route; + + await _client.ExportAsync(_filePath); + + Assert.AreEqual($"Bearer {ApiToken}", _httpClient.DefaultRequestHeaders.Authorization.ToString()); + Assert.AreEqual($"{_settings.ApiUrl}", _httpClient.BaseAddress.ToString()); + Assert.AreEqual(HttpMethod.Post, _httpClient.Requests[0].Method); + Assert.AreEqual(expectedRoute, _httpClient.Requests[0].Uri); + Assert.AreEqual(json, await _httpClient.Requests[0].Content.ReadAsStringAsync()); + } + } +}