diff --git a/BinDays.Api.Collectors/Collectors/Councils/Mansfield.cs b/BinDays.Api.Collectors/Collectors/Councils/Mansfield.cs new file mode 100644 index 00000000..155e98d9 --- /dev/null +++ b/BinDays.Api.Collectors/Collectors/Councils/Mansfield.cs @@ -0,0 +1,241 @@ +namespace BinDays.Api.Collectors.Collectors.Councils; + +using BinDays.Api.Collectors.Collectors.Vendors; +using BinDays.Api.Collectors.Models; +using BinDays.Api.Collectors.Utilities; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text.Json; +using System.Text.RegularExpressions; + +/// +/// Collector implementation for Mansfield. +/// +internal sealed partial class Mansfield : GovUkCollectorBase, ICollector +{ + /// + public string Name => "Mansfield"; + + /// + public Uri WebsiteUrl => new("https://www.mansfield.gov.uk/"); + + /// + public override string GovUkId => "mansfield"; + + /// + /// The list of bin types for this collector. + /// + private readonly IReadOnlyCollection _binTypes = [ + new() + { + Name = "General Waste", + Colour = BinColour.Green, + Keys = [ "General Waste Collection Service" ], + }, + new() + { + Name = "Recycling", + Colour = BinColour.Blue, + Keys = [ "Recycling Waste Collection Service" ], + }, + new() + { + Name = "Garden Waste", + Colour = BinColour.Brown, + Keys = [ "Garden Waste Collection Service" ], + }, + new() + { + Name = "Glass", + Colour = BinColour.Black, + Keys = [ "Glass Waste Collection Service" ], + }, + ]; + + /// + /// The form URL for the collector. + /// + private const string _formUrl = "https://www.mansfield.gov.uk/xfp/form/1339"; + + /// + /// Regex for the token from the form. + /// + [GeneratedRegex(@"name=""__token"" value=""(?[^""]+)""")] + private static partial Regex TokenRegex(); + + /// + /// Regex for the addresses from the data. + /// + [GeneratedRegex(@"\d+)""[^>]*>\s*(?
[^<]+?)\s*")] + private static partial Regex AddressRegex(); + + /// + public GetAddressesResponse GetAddresses(string postcode, ClientSideResponse? clientSideResponse) + { + // Prepare client-side request for getting token + if (clientSideResponse == null) + { + var clientSideRequest = new ClientSideRequest + { + RequestId = 1, + Url = _formUrl, + Method = "GET", + Headers = new() { + {"user-agent", Constants.UserAgent}, + }, + }; + + var getAddressesResponse = new GetAddressesResponse + { + NextClientSideRequest = clientSideRequest, + }; + + return getAddressesResponse; + } + // Prepare client-side request for getting addresses + else if (clientSideResponse.RequestId == 1) + { + var token = TokenRegex().Match(clientSideResponse.Content).Groups["token"].Value; + var setCookieHeader = clientSideResponse.Headers["set-cookie"]!; + var requestCookies = ProcessingUtilities.ParseSetCookieHeaderForRequestCookie(setCookieHeader); + + var requestBody = ProcessingUtilities.ConvertDictionaryToFormData(new() + { + {"__token", token}, + {"page", "2615"}, + {"locale", "en_GB"}, + {"injectedParams", "{\"formID\":\"1339\"}"}, + {"q3fc8e993e4e89b244317c1f13b6d65c0b0ef1ad2_0_0", postcode}, + {"callback", "{\"action\":\"ic\",\"element\":\"q3fc8e993e4e89b244317c1f13b6d65c0b0ef1ad2\",\"data\":0,\"tableRow\":-1}"}, + {"q177fee160e3d7694451f7d047342e9c0e3ce01c9", string.Empty}, + }); + + var clientSideRequest = new ClientSideRequest + { + RequestId = 2, + Url = _formUrl, + Method = "POST", + Headers = new() { + {"user-agent", Constants.UserAgent}, + {"content-type", "application/x-www-form-urlencoded"}, + {"cookie", requestCookies}, + }, + Body = requestBody, + }; + + var getAddressesResponse = new GetAddressesResponse + { + NextClientSideRequest = clientSideRequest, + }; + + return getAddressesResponse; + } + // Process addresses from response + else if (clientSideResponse.RequestId == 2) + { + var rawAddresses = AddressRegex().Matches(clientSideResponse.Content)!; + + // Iterate through each address, and create a new address object + var addresses = new List
(); + foreach (Match rawAddress in rawAddresses) + { + var uid = rawAddress.Groups["uid"].Value; + + if (string.IsNullOrWhiteSpace(uid) || uid == "0") + { + continue; + } + + var address = new Address + { + Property = rawAddress.Groups["address"].Value.Trim(), + Postcode = postcode, + Uid = uid, + }; + + addresses.Add(address); + } + + var getAddressesResponse = new GetAddressesResponse + { + Addresses = [.. addresses], + }; + + return getAddressesResponse; + } + + throw new InvalidOperationException("Invalid client-side request."); + } + + /// + public GetBinDaysResponse GetBinDays(Address address, ClientSideResponse? clientSideResponse) + { + // Prepare client-side request for getting bin days + if (clientSideResponse == null) + { + var fromDate = DateOnly.FromDateTime(DateTime.Now); + var toDate = fromDate.AddDays(364); + + var requestUrl = $"https://portal.mansfield.gov.uk/mdcwhitespacewebservice/WhiteSpaceWS.asmx/GetCollectionByUPRNAndDatePlus?&apiKey=mDc-wN3-B0f-f4P&UPRN={address.Uid}&ColFromDate={fromDate:yyyy-MM-dd}&ColToDate={toDate:yyyy-MM-dd}"; + + var clientSideRequest = new ClientSideRequest + { + RequestId = 1, + Url = requestUrl, + Method = "GET", + Headers = new() { + {"user-agent", Constants.UserAgent}, + }, + }; + + var getBinDaysResponse = new GetBinDaysResponse + { + NextClientSideRequest = clientSideRequest, + }; + + return getBinDaysResponse; + } + // Process bin days from response + else if (clientSideResponse.RequestId == 1) + { + using var jsonDoc = JsonDocument.Parse(clientSideResponse.Content); + var collections = jsonDoc.RootElement.GetProperty("Collections"); + + // Iterate through each bin day, and create a new bin day object + var binDays = new List(); + foreach (var collection in collections.EnumerateArray()) + { + var service = collection.GetProperty("Service").GetString()!; + var dateString = collection.GetProperty("Date").GetString()!; + + var collectionDate = DateTime.ParseExact( + dateString, + "dd/MM/yyyy HH:mm:ss", + CultureInfo.InvariantCulture, + DateTimeStyles.None + ); + + var matchedBinTypes = ProcessingUtilities.GetMatchingBins(_binTypes, service); + + var binDay = new BinDay + { + Date = DateOnly.FromDateTime(collectionDate), + Address = address, + Bins = matchedBinTypes, + }; + + binDays.Add(binDay); + } + + var getBinDaysResponse = new GetBinDaysResponse + { + BinDays = ProcessingUtilities.ProcessBinDays(binDays), + }; + + return getBinDaysResponse; + } + + throw new InvalidOperationException("Invalid client-side request."); + } +} diff --git a/BinDays.Api.IntegrationTests/Collectors/Councils/MansfieldTests.cs b/BinDays.Api.IntegrationTests/Collectors/Councils/MansfieldTests.cs new file mode 100644 index 00000000..e78b51dc --- /dev/null +++ b/BinDays.Api.IntegrationTests/Collectors/Councils/MansfieldTests.cs @@ -0,0 +1,36 @@ +namespace BinDays.Api.IntegrationTests.Collectors.Councils; + +using BinDays.Api.Collectors.Collectors; +using BinDays.Api.Collectors.Collectors.Councils; +using BinDays.Api.Collectors.Services; +using BinDays.Api.IntegrationTests.Helpers; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +public class MansfieldTests +{ + private readonly IntegrationTestClient _client; + private static readonly ICollector _collector = new Mansfield(); + private readonly CollectorService _collectorService = new([_collector]); + private readonly ITestOutputHelper _outputHelper; + + public MansfieldTests(ITestOutputHelper outputHelper) + { + _outputHelper = outputHelper; + _client = new IntegrationTestClient(outputHelper); + } + + [Theory] + [InlineData("NG19 8PL")] + public async Task GetBinDaysTest(string postcode) + { + await TestSteps.EndToEnd( + _client, + _collectorService, + _collector, + postcode, + _outputHelper + ); + } +}