Merge pull request #12 from StevanFreeborn/stevanfreeborn/fix/11-avoid-duplicate-stream-notifications

feat(youtube): multiple stream notifications
This commit is contained in:
Stevan Freeborn
2026-01-11 10:36:30 -06:00
committed by GitHub
5 changed files with 97 additions and 40 deletions
@@ -12,6 +12,8 @@ internal static class NotificationHandler
CancellationToken cancellationToken = default
)
{
lastPostedStreamStore.RemoveValuesOlderThan24Hours();
using StreamReader stream = new(context.Request.Body);
var body = await stream.ReadToEndAsync(cancellationToken);
@@ -4,4 +4,5 @@ internal interface ILastPostedStreamStore
{
void SetValue(string value);
bool HasValue(string value);
void RemoveValuesOlderThan24Hours();
}
@@ -1,18 +1,45 @@
namespace StevesBot.Webhook.YouTube;
internal class LastPostedStreamStore : ILastPostedStreamStore
internal class LastPostedStreamStore(TimeProvider timeProvider) : ILastPostedStreamStore
{
private string _value = string.Empty;
private readonly TimeProvider _timeProvider = timeProvider;
private readonly ConcurrentDictionary<string, long> _values = [];
public void SetValue(string value)
{
ArgumentNullException.ThrowIfNull(value);
_value = value;
_values[value] = _timeProvider.GetUtcNow()
.ToUnixTimeMilliseconds();
}
public bool HasValue(string value)
{
ArgumentNullException.ThrowIfNull(value);
return _value.Equals(value, StringComparison.OrdinalIgnoreCase);
return _values.TryGetValue(value, out var _);
}
public void RemoveValuesOlderThan24Hours()
{
var currentTime = _timeProvider.GetUtcNow();
var twentyFourHoursAgo = currentTime
.AddHours(-24)
.ToUnixTimeMilliseconds();
var keysToRemove = new List<string>();
foreach (var (k, v) in _values)
{
if (v > twentyFourHoursAgo)
{
continue;
}
keysToRemove.Add(k);
}
foreach (var k in keysToRemove)
{
_values.Remove(k, out var _);
}
}
}
@@ -1,36 +0,0 @@
namespace StevesBot.Webhook.Tests.Unit;
public class LastPostedStreamStoreTests
{
[Fact]
public void SetValue_WhenCalledWithNull_ItShouldThrowArgumentNullException()
{
var store = new LastPostedStreamStore();
var act = () => store.SetValue(null!);
act.Should().Throw<ArgumentNullException>();
}
[Fact]
public void HasValue_WhenCalledWithNull_ItShouldThrowArgumentNullException()
{
var store = new LastPostedStreamStore();
var act = () => store.HasValue(null!);
act.Should().Throw<ArgumentNullException>();
}
[Fact]
public void SetValueHasValue_WhenCalled_ItShouldStoreValue()
{
var store = new LastPostedStreamStore();
var streamId = "test-stream-id";
store.SetValue(streamId);
store.HasValue(streamId).Should().BeTrue();
}
}
@@ -0,0 +1,63 @@
namespace StevesBot.Webhook.Tests.Unit;
public class LastPostedStreamStoreTests
{
private readonly Mock<TimeProvider> _mockTimeProvider = new();
private readonly LastPostedStreamStore _sut;
public LastPostedStreamStoreTests()
{
_sut = new(_mockTimeProvider.Object);
}
[Fact]
public void SetValue_WhenCalledWithNull_ItShouldThrowArgumentNullException()
{
var act = () => _sut.SetValue(null!);
act.Should().Throw<ArgumentNullException>();
}
[Fact]
public void HasValue_WhenCalledWithNull_ItShouldThrowArgumentNullException()
{
var act = () => _sut.HasValue(null!);
act.Should().Throw<ArgumentNullException>();
}
[Fact]
public void SetValueHasValue_WhenCalled_ItShouldStoreValue()
{
var streamId = "test-stream-id";
_sut.SetValue(streamId);
_sut.HasValue(streamId).Should().BeTrue();
}
[Fact]
public void RemoveValuesOlderThan24Hours_WhenCalled_ItShouldRemoveItemsOlderThan24Hours()
{
var oldStreamId = "test-stream-id-old";
var currentStreamId = "test-stream-id-current";
var currentTime = DateTimeOffset.UtcNow;
var invalidTime = DateTimeOffset.UtcNow.AddHours(-25);
var validTime = DateTimeOffset.UtcNow.AddHours(-10);
_mockTimeProvider.SetupSequence(static m => m.GetUtcNow())
.Returns(invalidTime)
.Returns(validTime)
.Returns(currentTime);
_sut.SetValue(oldStreamId);
_sut.SetValue(currentStreamId);
_sut.RemoveValuesOlderThan24Hours();
_sut.HasValue(oldStreamId).Should().BeFalse();
_sut.HasValue(currentStreamId).Should().BeTrue();
}
}