From 679b1a2002220bea610a2482e2a7e543f37d9b6a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 11 Jan 2026 10:19:21 +0000 Subject: [PATCH 1/5] Add collector for Mansfield Closes #84 Generated with Codex CLI --- .../Collectors/Councils/Mansfield.cs | 223 ++++++++++++++++++ .../Collectors/Councils/MansfieldTests.cs | 36 +++ 2 files changed, 259 insertions(+) create mode 100644 BinDays.Api.Collectors/Collectors/Councils/Mansfield.cs create mode 100644 BinDays.Api.IntegrationTests/Collectors/Councils/MansfieldTests.cs diff --git a/BinDays.Api.Collectors/Collectors/Councils/Mansfield.cs b/BinDays.Api.Collectors/Collectors/Councils/Mansfield.cs new file mode 100644 index 00000000..af3ad779 --- /dev/null +++ b/BinDays.Api.Collectors/Collectors/Councils/Mansfield.cs @@ -0,0 +1,223 @@ +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"; + + private const string FormUrl = "https://www.mansfield.gov.uk/xfp/form/1339"; + private const string FormPage = "2615"; + private const string FormId = "1339"; + private const string ApiUrl = "https://portal.mansfield.gov.uk/mdcwhitespacewebservice/WhiteSpaceWS.asmx/GetCollectionByUPRNAndDatePlus"; + private const string ApiKey = "mDc-wN3-B0f-f4P"; + + 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" ], + }, + ]; + + [GeneratedRegex(@"name=""__token"" value=""(?[^""]+)""")] + private static partial Regex TokenRegex(); + + [GeneratedRegex(@"\d+)""[^>]*>\s*(?
[^<]+?)\s*")] + private static partial Regex AddressRegex(); + + /// + public GetAddressesResponse GetAddresses(string postcode, ClientSideResponse? clientSideResponse) + { + var formattedPostcode = ProcessingUtilities.FormatPostcode(postcode); + + // 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}, + }, + }; + + return new GetAddressesResponse + { + NextClientSideRequest = clientSideRequest + }; + } + // Prepare client-side request for getting addresses + else if (clientSideResponse.RequestId == 1) + { + var token = TokenRegex().Match(clientSideResponse.Content).Groups["token"].Value; + var requestCookies = ProcessingUtilities.ParseSetCookieHeaderForRequestCookie(clientSideResponse.Headers["set-cookie"]); + + var requestBody = ProcessingUtilities.ConvertDictionaryToFormData(new() + { + {"__token", token}, + {"page", FormPage}, + {"locale", "en_GB"}, + {"injectedParams", $"{{\"formID\":\"{FormId}\"}}"}, + {"q3fc8e993e4e89b244317c1f13b6d65c0b0ef1ad2_0_0", formattedPostcode}, + {"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, + }; + + return new GetAddressesResponse + { + NextClientSideRequest = clientSideRequest + }; + } + // Process addresses from response + else if (clientSideResponse.RequestId == 2) + { + var addresses = new List
(); + var rawAddresses = AddressRegex().Matches(clientSideResponse.Content)!; + + 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 = formattedPostcode, + Uid = uid, + }; + + addresses.Add(address); + } + + return new GetAddressesResponse + { + Addresses = [.. addresses], + }; + } + + 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 = $"{ApiUrl}?&apiKey={ApiKey}&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}, + }, + }; + + return new GetBinDaysResponse + { + NextClientSideRequest = clientSideRequest + }; + } + // Process bin days from response + else if (clientSideResponse.RequestId == 1) + { + using var jsonDoc = JsonDocument.Parse(clientSideResponse.Content); + var collections = jsonDoc.RootElement.GetProperty("Collections"); + + 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); + } + + return new GetBinDaysResponse + { + BinDays = ProcessingUtilities.ProcessBinDays(binDays), + }; + } + + 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..77893a33 --- /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 + ); + } +} From 5e8d1e9c7bd7be0f610e8ce836b16087afb26fd9 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 11 Jan 2026 10:19:59 +0000 Subject: [PATCH 2/5] Auto-format code with dotnet format --- .../Collectors/Councils/Mansfield.cs | 446 +++++++++--------- .../Collectors/Councils/MansfieldTests.cs | 72 +-- 2 files changed, 259 insertions(+), 259 deletions(-) diff --git a/BinDays.Api.Collectors/Collectors/Councils/Mansfield.cs b/BinDays.Api.Collectors/Collectors/Councils/Mansfield.cs index af3ad779..94b54333 100644 --- a/BinDays.Api.Collectors/Collectors/Councils/Mansfield.cs +++ b/BinDays.Api.Collectors/Collectors/Councils/Mansfield.cs @@ -1,223 +1,223 @@ -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"; - - private const string FormUrl = "https://www.mansfield.gov.uk/xfp/form/1339"; - private const string FormPage = "2615"; - private const string FormId = "1339"; - private const string ApiUrl = "https://portal.mansfield.gov.uk/mdcwhitespacewebservice/WhiteSpaceWS.asmx/GetCollectionByUPRNAndDatePlus"; - private const string ApiKey = "mDc-wN3-B0f-f4P"; - - 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" ], - }, - ]; - - [GeneratedRegex(@"name=""__token"" value=""(?[^""]+)""")] - private static partial Regex TokenRegex(); - - [GeneratedRegex(@"\d+)""[^>]*>\s*(?
[^<]+?)\s*")] - private static partial Regex AddressRegex(); - - /// - public GetAddressesResponse GetAddresses(string postcode, ClientSideResponse? clientSideResponse) - { - var formattedPostcode = ProcessingUtilities.FormatPostcode(postcode); - - // 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}, - }, - }; - - return new GetAddressesResponse - { - NextClientSideRequest = clientSideRequest - }; - } - // Prepare client-side request for getting addresses - else if (clientSideResponse.RequestId == 1) - { - var token = TokenRegex().Match(clientSideResponse.Content).Groups["token"].Value; - var requestCookies = ProcessingUtilities.ParseSetCookieHeaderForRequestCookie(clientSideResponse.Headers["set-cookie"]); - - var requestBody = ProcessingUtilities.ConvertDictionaryToFormData(new() - { - {"__token", token}, - {"page", FormPage}, - {"locale", "en_GB"}, - {"injectedParams", $"{{\"formID\":\"{FormId}\"}}"}, - {"q3fc8e993e4e89b244317c1f13b6d65c0b0ef1ad2_0_0", formattedPostcode}, - {"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, - }; - - return new GetAddressesResponse - { - NextClientSideRequest = clientSideRequest - }; - } - // Process addresses from response - else if (clientSideResponse.RequestId == 2) - { - var addresses = new List
(); - var rawAddresses = AddressRegex().Matches(clientSideResponse.Content)!; - - 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 = formattedPostcode, - Uid = uid, - }; - - addresses.Add(address); - } - - return new GetAddressesResponse - { - Addresses = [.. addresses], - }; - } - - 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 = $"{ApiUrl}?&apiKey={ApiKey}&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}, - }, - }; - - return new GetBinDaysResponse - { - NextClientSideRequest = clientSideRequest - }; - } - // Process bin days from response - else if (clientSideResponse.RequestId == 1) - { - using var jsonDoc = JsonDocument.Parse(clientSideResponse.Content); - var collections = jsonDoc.RootElement.GetProperty("Collections"); - - 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); - } - - return new GetBinDaysResponse - { - BinDays = ProcessingUtilities.ProcessBinDays(binDays), - }; - } - - throw new InvalidOperationException("Invalid client-side request."); - } -} +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"; + + private const string FormUrl = "https://www.mansfield.gov.uk/xfp/form/1339"; + private const string FormPage = "2615"; + private const string FormId = "1339"; + private const string ApiUrl = "https://portal.mansfield.gov.uk/mdcwhitespacewebservice/WhiteSpaceWS.asmx/GetCollectionByUPRNAndDatePlus"; + private const string ApiKey = "mDc-wN3-B0f-f4P"; + + 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" ], + }, + ]; + + [GeneratedRegex(@"name=""__token"" value=""(?[^""]+)""")] + private static partial Regex TokenRegex(); + + [GeneratedRegex(@"\d+)""[^>]*>\s*(?
[^<]+?)\s*")] + private static partial Regex AddressRegex(); + + /// + public GetAddressesResponse GetAddresses(string postcode, ClientSideResponse? clientSideResponse) + { + var formattedPostcode = ProcessingUtilities.FormatPostcode(postcode); + + // 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}, + }, + }; + + return new GetAddressesResponse + { + NextClientSideRequest = clientSideRequest + }; + } + // Prepare client-side request for getting addresses + else if (clientSideResponse.RequestId == 1) + { + var token = TokenRegex().Match(clientSideResponse.Content).Groups["token"].Value; + var requestCookies = ProcessingUtilities.ParseSetCookieHeaderForRequestCookie(clientSideResponse.Headers["set-cookie"]); + + var requestBody = ProcessingUtilities.ConvertDictionaryToFormData(new() + { + {"__token", token}, + {"page", FormPage}, + {"locale", "en_GB"}, + {"injectedParams", $"{{\"formID\":\"{FormId}\"}}"}, + {"q3fc8e993e4e89b244317c1f13b6d65c0b0ef1ad2_0_0", formattedPostcode}, + {"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, + }; + + return new GetAddressesResponse + { + NextClientSideRequest = clientSideRequest + }; + } + // Process addresses from response + else if (clientSideResponse.RequestId == 2) + { + var addresses = new List
(); + var rawAddresses = AddressRegex().Matches(clientSideResponse.Content)!; + + 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 = formattedPostcode, + Uid = uid, + }; + + addresses.Add(address); + } + + return new GetAddressesResponse + { + Addresses = [.. addresses], + }; + } + + 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 = $"{ApiUrl}?&apiKey={ApiKey}&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}, + }, + }; + + return new GetBinDaysResponse + { + NextClientSideRequest = clientSideRequest + }; + } + // Process bin days from response + else if (clientSideResponse.RequestId == 1) + { + using var jsonDoc = JsonDocument.Parse(clientSideResponse.Content); + var collections = jsonDoc.RootElement.GetProperty("Collections"); + + 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); + } + + return new GetBinDaysResponse + { + BinDays = ProcessingUtilities.ProcessBinDays(binDays), + }; + } + + 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 index 77893a33..e78b51dc 100644 --- a/BinDays.Api.IntegrationTests/Collectors/Councils/MansfieldTests.cs +++ b/BinDays.Api.IntegrationTests/Collectors/Councils/MansfieldTests.cs @@ -1,36 +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 - ); - } -} +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 + ); + } +} From ecff286b0cf21a5b97a461ef33936b8cca3739bc Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Sun, 1 Feb 2026 23:12:10 +0000 Subject: [PATCH 3/5] Address PR review comments for Mansfield collector - Remove FormatPostcode call (postcode is already formatted) - Reorder class members (_binTypes before const fields) - Inline single-use constants (FormPage, FormId, ApiUrl) - Add trailing commas to multi-line initializers - Use separate variable declarations instead of inline returns - Add standard comments before foreach loops - Use TryGetValue for header access instead of direct indexer - Add XML documentation comments for private fields and regex methods Co-authored-by: Andrew Riggs --- .../Collectors/Councils/Mansfield.cs | 68 ++++++++++++------- 1 file changed, 42 insertions(+), 26 deletions(-) diff --git a/BinDays.Api.Collectors/Collectors/Councils/Mansfield.cs b/BinDays.Api.Collectors/Collectors/Councils/Mansfield.cs index 94b54333..9204346a 100644 --- a/BinDays.Api.Collectors/Collectors/Councils/Mansfield.cs +++ b/BinDays.Api.Collectors/Collectors/Councils/Mansfield.cs @@ -23,12 +23,9 @@ internal sealed partial class Mansfield : GovUkCollectorBase, ICollector /// public override string GovUkId => "mansfield"; - private const string FormUrl = "https://www.mansfield.gov.uk/xfp/form/1339"; - private const string FormPage = "2615"; - private const string FormId = "1339"; - private const string ApiUrl = "https://portal.mansfield.gov.uk/mdcwhitespacewebservice/WhiteSpaceWS.asmx/GetCollectionByUPRNAndDatePlus"; - private const string ApiKey = "mDc-wN3-B0f-f4P"; - + /// + /// The list of bin types for this collector. + /// private readonly IReadOnlyCollection _binTypes = [ new() { @@ -56,48 +53,58 @@ internal sealed partial class Mansfield : GovUkCollectorBase, ICollector }, ]; + private const string _formUrl = "https://www.mansfield.gov.uk/xfp/form/1339"; + private const string _apiKey = "mDc-wN3-B0f-f4P"; + + /// + /// 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) { - var formattedPostcode = ProcessingUtilities.FormatPostcode(postcode); - // Prepare client-side request for getting token if (clientSideResponse == null) { var clientSideRequest = new ClientSideRequest { RequestId = 1, - Url = FormUrl, + Url = _formUrl, Method = "GET", Headers = new() { {"user-agent", Constants.UserAgent}, }, }; - return new GetAddressesResponse + var getAddressesResponse = new GetAddressesResponse { - NextClientSideRequest = clientSideRequest + 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 requestCookies = ProcessingUtilities.ParseSetCookieHeaderForRequestCookie(clientSideResponse.Headers["set-cookie"]); + clientSideResponse.Headers.TryGetValue("set-cookie", out var setCookieHeader); + var requestCookies = ProcessingUtilities.ParseSetCookieHeaderForRequestCookie(setCookieHeader!); var requestBody = ProcessingUtilities.ConvertDictionaryToFormData(new() { {"__token", token}, - {"page", FormPage}, + {"page", "2615"}, {"locale", "en_GB"}, - {"injectedParams", $"{{\"formID\":\"{FormId}\"}}"}, - {"q3fc8e993e4e89b244317c1f13b6d65c0b0ef1ad2_0_0", formattedPostcode}, + {"injectedParams", "{\"formID\":\"1339\"}"}, + {"q3fc8e993e4e89b244317c1f13b6d65c0b0ef1ad2_0_0", postcode}, {"callback", "{\"action\":\"ic\",\"element\":\"q3fc8e993e4e89b244317c1f13b6d65c0b0ef1ad2\",\"data\":0,\"tableRow\":-1}"}, {"q177fee160e3d7694451f7d047342e9c0e3ce01c9", string.Empty}, }); @@ -105,7 +112,7 @@ public GetAddressesResponse GetAddresses(string postcode, ClientSideResponse? cl var clientSideRequest = new ClientSideRequest { RequestId = 2, - Url = FormUrl, + Url = _formUrl, Method = "POST", Headers = new() { {"user-agent", Constants.UserAgent}, @@ -115,17 +122,20 @@ public GetAddressesResponse GetAddresses(string postcode, ClientSideResponse? cl Body = requestBody, }; - return new GetAddressesResponse + var getAddressesResponse = new GetAddressesResponse { - NextClientSideRequest = clientSideRequest + NextClientSideRequest = clientSideRequest, }; + + return getAddressesResponse; } // Process addresses from response else if (clientSideResponse.RequestId == 2) { - var addresses = new List
(); 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; @@ -138,17 +148,19 @@ public GetAddressesResponse GetAddresses(string postcode, ClientSideResponse? cl var address = new Address { Property = rawAddress.Groups["address"].Value.Trim(), - Postcode = formattedPostcode, + Postcode = postcode, Uid = uid, }; addresses.Add(address); } - return new GetAddressesResponse + var getAddressesResponse = new GetAddressesResponse { Addresses = [.. addresses], }; + + return getAddressesResponse; } throw new InvalidOperationException("Invalid client-side request."); @@ -163,7 +175,7 @@ public GetBinDaysResponse GetBinDays(Address address, ClientSideResponse? client var fromDate = DateOnly.FromDateTime(DateTime.Now); var toDate = fromDate.AddDays(364); - var requestUrl = $"{ApiUrl}?&apiKey={ApiKey}&UPRN={address.Uid}&ColFromDate={fromDate:yyyy-MM-dd}&ColToDate={toDate:yyyy-MM-dd}"; + var requestUrl = $"https://portal.mansfield.gov.uk/mdcwhitespacewebservice/WhiteSpaceWS.asmx/GetCollectionByUPRNAndDatePlus?&apiKey={_apiKey}&UPRN={address.Uid}&ColFromDate={fromDate:yyyy-MM-dd}&ColToDate={toDate:yyyy-MM-dd}"; var clientSideRequest = new ClientSideRequest { @@ -175,10 +187,12 @@ public GetBinDaysResponse GetBinDays(Address address, ClientSideResponse? client }, }; - return new GetBinDaysResponse + var getBinDaysResponse = new GetBinDaysResponse { - NextClientSideRequest = clientSideRequest + NextClientSideRequest = clientSideRequest, }; + + return getBinDaysResponse; } // Process bin days from response else if (clientSideResponse.RequestId == 1) @@ -186,8 +200,8 @@ public GetBinDaysResponse GetBinDays(Address address, ClientSideResponse? client 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()!; @@ -212,10 +226,12 @@ public GetBinDaysResponse GetBinDays(Address address, ClientSideResponse? client binDays.Add(binDay); } - return new GetBinDaysResponse + var getBinDaysResponse = new GetBinDaysResponse { BinDays = ProcessingUtilities.ProcessBinDays(binDays), }; + + return getBinDaysResponse; } throw new InvalidOperationException("Invalid client-side request."); From c77eff27207560a72b4efa5e852ca1ff3b173c79 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Sun, 1 Feb 2026 23:21:48 +0000 Subject: [PATCH 4/5] Address final PR review comments - Add XML summary docstring for _formUrl const field - Remove single-use _apiKey const and inline the value - TryGetValue usage confirmed correct per style guide Co-authored-by: Andrew Riggs --- BinDays.Api.Collectors/Collectors/Councils/Mansfield.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/BinDays.Api.Collectors/Collectors/Councils/Mansfield.cs b/BinDays.Api.Collectors/Collectors/Councils/Mansfield.cs index 9204346a..362ad370 100644 --- a/BinDays.Api.Collectors/Collectors/Councils/Mansfield.cs +++ b/BinDays.Api.Collectors/Collectors/Councils/Mansfield.cs @@ -53,8 +53,10 @@ internal sealed partial class Mansfield : GovUkCollectorBase, ICollector }, ]; + /// + /// The form URL for the collector. + /// private const string _formUrl = "https://www.mansfield.gov.uk/xfp/form/1339"; - private const string _apiKey = "mDc-wN3-B0f-f4P"; /// /// Regex for the token from the form. @@ -175,7 +177,7 @@ public GetBinDaysResponse GetBinDays(Address address, ClientSideResponse? client var fromDate = DateOnly.FromDateTime(DateTime.Now); var toDate = fromDate.AddDays(364); - var requestUrl = $"https://portal.mansfield.gov.uk/mdcwhitespacewebservice/WhiteSpaceWS.asmx/GetCollectionByUPRNAndDatePlus?&apiKey={_apiKey}&UPRN={address.Uid}&ColFromDate={fromDate:yyyy-MM-dd}&ColToDate={toDate:yyyy-MM-dd}"; + 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 { From 99d160dfc601cd37777cd12f4319062f72d5bfd8 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Sun, 1 Feb 2026 23:28:22 +0000 Subject: [PATCH 5/5] Use direct header access with null-forgiving operator for set-cookie Changed from TryGetValue pattern to direct indexer access with ! operator since the set-cookie header is required for this collector. Co-authored-by: Andrew Riggs --- BinDays.Api.Collectors/Collectors/Councils/Mansfield.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BinDays.Api.Collectors/Collectors/Councils/Mansfield.cs b/BinDays.Api.Collectors/Collectors/Councils/Mansfield.cs index 362ad370..155e98d9 100644 --- a/BinDays.Api.Collectors/Collectors/Councils/Mansfield.cs +++ b/BinDays.Api.Collectors/Collectors/Councils/Mansfield.cs @@ -97,8 +97,8 @@ public GetAddressesResponse GetAddresses(string postcode, ClientSideResponse? cl else if (clientSideResponse.RequestId == 1) { var token = TokenRegex().Match(clientSideResponse.Content).Groups["token"].Value; - clientSideResponse.Headers.TryGetValue("set-cookie", out var setCookieHeader); - var requestCookies = ProcessingUtilities.ParseSetCookieHeaderForRequestCookie(setCookieHeader!); + var setCookieHeader = clientSideResponse.Headers["set-cookie"]!; + var requestCookies = ProcessingUtilities.ParseSetCookieHeaderForRequestCookie(setCookieHeader); var requestBody = ProcessingUtilities.ConvertDictionaryToFormData(new() {