Skip to content

Commit 56ec2f4

Browse files
committed
Luhn invalid check logs warning
1 parent 1b3b968 commit 56ec2f4

2 files changed

Lines changed: 56 additions & 9 deletions

File tree

src/api/Nhs.Appointments.Core/ReferenceNumber/V2/Provider.cs

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Text.RegularExpressions;
55
using LuhnNet;
66
using Microsoft.Extensions.Caching.Memory;
7+
using Microsoft.Extensions.Logging;
78
using Microsoft.Extensions.Options;
89

910
namespace Nhs.Appointments.Core.ReferenceNumber.V2;
@@ -26,12 +27,15 @@ public class Provider(
2627
IOptions<ReferenceNumberOptions> options,
2728
IBookingReferenceDocumentStore bookingReferenceDocumentStore,
2829
IMemoryCache memoryCache,
30+
ILogger<Provider> logger,
2931
TimeProvider timeProvider)
3032
: IProvider
3133
{
32-
private Regex BookingReferenceV2Regex => new (@"^\d{4}-\d{5}-\d{4}$");
34+
//needed for backwards compatibility since we are now checking correctly formatted strings
3335
private Regex BookingReferenceV1Regex => new (@"^\d{2}-\d{2}-\d{6}$");
3436

37+
private Regex BookingReferenceV2Regex => new (@"^\d{4}-\d{5}-\d{4}$");
38+
3539
private IOptions<ReferenceNumberOptions> Options { get; } = options;
3640

3741
//in generating the partition key, splitting the current day of the year into X buckets of this length
@@ -111,7 +115,7 @@ private int GetSequenceMultiplier(string partitionKey)
111115
}
112116

113117
sequenceMultiplier = DeriveSequenceMultiplier(partitionKey);
114-
memoryCache.Set(cacheKey, sequenceMultiplier, TimeSpan.FromDays(PartitionBucketLengthInDays + 4)); // easily spans the partition length
118+
memoryCache.Set(cacheKey, sequenceMultiplier, TimeSpan.FromDays(PartitionBucketLengthInDays + 2)); // easily spans the partition length
115119
return sequenceMultiplier;
116120
}
117121

@@ -147,9 +151,13 @@ public int DeriveSequenceMultiplier(string partitionKey)
147151
//confirmation of GCD(multiplier,SequenceMax) == 1, without this, the entire bijection pattern fails!!
148152
if ((int)BigInteger.GreatestCommonDivisor(multiplier, SequenceMax) != 1)
149153
{
154+
var message =
155+
$"CRITICAL ERROR - Derived sequence multiplier does not pass logical requirement of GCD(multiplier,SequenceMax) == 1. " +
156+
$"Failure for multiplier: '{multiplier}' and sequenceMax: '{SequenceMax}'";
157+
150158
//fail fast - logically this should NEVER happen and something has gone VERY WRONG!
151-
throw new InvalidOperationException($"CRITICAL ERROR - Derived sequence multiplier does not pass logical requirement of GCD(multiplier,SequenceMax) == 1. " +
152-
$"Failure for multiplier: '{multiplier}' and sequenceMax: '{SequenceMax}'");
159+
logger.LogCritical(message);
160+
throw new InvalidOperationException(message);
153161
}
154162

155163
//GCD(multiplier,SequenceMax) == 1 guaranteed
@@ -181,8 +189,11 @@ public bool IsValidBookingReference(string bookingReference)
181189

182190
if (!Luhn.IsValid(digitReference))
183191
{
184-
// Logger.LogWarning
185-
// checksum fail indicates someone tried to guess a booking reference format using the valid format
192+
// checksum fail indicates someone may have tried to guess a booking reference format using the valid format
193+
// this could however also be a user typing error
194+
195+
//if there are a lot of these warnings, could this suggest someone trying to brute force guess a valid reference...?
196+
logger.LogWarning("Booking Reference '{BookingReference}' does not pass the valid Luhn digit requirement.", bookingReference);
186197
return false;
187198
}
188199

tests/Nhs.Appointments.Core.UnitTests/ReferenceNumber/V2/ProviderTests.cs

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Microsoft.Extensions.Caching.Memory;
2+
using Microsoft.Extensions.Logging;
23
using Microsoft.Extensions.Options;
34
using Nhs.Appointments.Core.ReferenceNumber.V2;
45

@@ -16,14 +17,15 @@ public class ProviderTests
1617

1718
private readonly Mock<IBookingReferenceDocumentStore> _bookingReferenceDocumentStore = new();
1819
private readonly Mock<TimeProvider> _timeProvider = new();
20+
private readonly Mock<ILogger<Provider>> _logger = new();
1921
private readonly Mock<IOptions<ReferenceNumberOptions>> _options = new();
2022

2123
private readonly IProvider _sut;
2224

2325
public ProviderTests()
2426
{
2527
_options.Setup(x => x.Value).Returns(new ReferenceNumberOptions { HmacKeyVersion = 1, HmacKey = HmacTestSecretKey});
26-
_sut = new Provider(_options.Object, _bookingReferenceDocumentStore.Object, new MemoryCache(new MemoryCacheOptions()), _timeProvider.Object);
28+
_sut = new Provider(_options.Object, _bookingReferenceDocumentStore.Object, new MemoryCache(new MemoryCacheOptions()), _logger.Object, _timeProvider.Object);
2729
}
2830

2931
[Fact]
@@ -135,26 +137,60 @@ public async Task GetReferenceNumber_GeneratesCorrectlyFormattedNumber_MinSequen
135137
}
136138

137139
[Fact]
138-
public async Task GetReferenceNumber_ChecksumDigitExists_MeansOnlyOneReferenceIsValid()
140+
public async Task GetReferenceNumber_LuhnChecksumDigitExists_MeansOnlyOneReferenceIsValid()
139141
{
140142
_timeProvider.Setup(x => x.GetUtcNow()).Returns(new DateTime(2016, 6, 06, 17, 23, 00));
141143
_bookingReferenceDocumentStore.Setup(x => x.GetNextSequenceNumber()).ReturnsAsync(76543789);
142144

143145
var result = await _sut.GetReferenceNumber();
144146
result.Should().Be("4016-90927-6174");
145147
Assert.True(_sut.IsValidBookingReference(result));
148+
149+
_logger.Verify(x => x.Log(
150+
LogLevel.Warning,
151+
It.IsAny<EventId>(),
152+
It.Is<It.IsAnyType>((state, t) =>
153+
state.ToString().Contains("Booking Reference '4016-90927-6174' does not pass the valid Luhn digit requirement.")
154+
),
155+
It.IsAny<Exception>(),
156+
It.IsAny<Func<It.IsAnyType, Exception, string>>()
157+
), Times.Never
158+
);
146159

147160
//the other 9 numbers with a different final digit should NOT be valid references
148161
Assert.False(_sut.IsValidBookingReference("4016-90927-6171"));
149162
Assert.False(_sut.IsValidBookingReference("4016-90927-6172"));
150163
Assert.False(_sut.IsValidBookingReference("4016-90927-6173"));
151-
152164
Assert.False(_sut.IsValidBookingReference("4016-90927-6175"));
153165
Assert.False(_sut.IsValidBookingReference("4016-90927-6176"));
154166
Assert.False(_sut.IsValidBookingReference("4016-90927-6177"));
155167
Assert.False(_sut.IsValidBookingReference("4016-90927-6178"));
156168
Assert.False(_sut.IsValidBookingReference("4016-90927-6179"));
157169
Assert.False(_sut.IsValidBookingReference("4016-90927-6170"));
170+
171+
VerifyLuhnDigitInvalidLogged("4016-90927-6171");
172+
VerifyLuhnDigitInvalidLogged("4016-90927-6172");
173+
VerifyLuhnDigitInvalidLogged("4016-90927-6173");
174+
VerifyLuhnDigitInvalidLogged("4016-90927-6175");
175+
VerifyLuhnDigitInvalidLogged("4016-90927-6176");
176+
VerifyLuhnDigitInvalidLogged("4016-90927-6177");
177+
VerifyLuhnDigitInvalidLogged("4016-90927-6178");
178+
VerifyLuhnDigitInvalidLogged("4016-90927-6179");
179+
VerifyLuhnDigitInvalidLogged("4016-90927-6170");
180+
}
181+
182+
private void VerifyLuhnDigitInvalidLogged(string bookingReference)
183+
{
184+
_logger.Verify(x => x.Log(
185+
LogLevel.Warning,
186+
It.IsAny<EventId>(),
187+
It.Is<It.IsAnyType>((state, t) =>
188+
state.ToString().Contains($"Booking Reference '{bookingReference}' does not pass the valid Luhn digit requirement.")
189+
),
190+
It.IsAny<Exception>(),
191+
It.IsAny<Func<It.IsAnyType, Exception, string>>()
192+
), Times.Once
193+
);
158194
}
159195

160196
[Fact]

0 commit comments

Comments
 (0)