diff --git a/src/src/StevesBot.Webhook/YouTube/Handlers/VerifySubscriptionHandler.cs b/src/src/StevesBot.Webhook/YouTube/Handlers/VerifySubscriptionHandler.cs index 506dac2..be548f6 100644 --- a/src/src/StevesBot.Webhook/YouTube/Handlers/VerifySubscriptionHandler.cs +++ b/src/src/StevesBot.Webhook/YouTube/Handlers/VerifySubscriptionHandler.cs @@ -10,7 +10,8 @@ internal static class VerifySubscriptionHandler [FromQuery(Name = "hub.lease_seconds")] string? leaseSeconds, [FromServices] IOptions subOptions, [FromServices] ILogger logger, - [FromServices] ConcurrentQueue subscriptionQueue + [FromServices] ConcurrentQueue subscriptionQueue, + [FromServices] TimeProvider timeProvider ) { if (mode is "denied") @@ -44,7 +45,7 @@ internal static class VerifySubscriptionHandler { CallbackUrl = subOptions.Value.CallbackUrl, TopicUrl = topic, - ExpiresAt = DateTime.UtcNow.AddSeconds(parsedSeconds), + ExpiresAt = timeProvider.GetUtcNow().AddSeconds(parsedSeconds), }; subscriptionQueue.Enqueue(task); diff --git a/src/tests/StevesBot.Webhook.Tests/Unit/VerifySubscriptionHandlerTests.cs b/src/tests/StevesBot.Webhook.Tests/Unit/VerifySubscriptionHandlerTests.cs index 75cb231..4c3f278 100644 --- a/src/tests/StevesBot.Webhook.Tests/Unit/VerifySubscriptionHandlerTests.cs +++ b/src/tests/StevesBot.Webhook.Tests/Unit/VerifySubscriptionHandlerTests.cs @@ -1,3 +1,5 @@ +using System.Globalization; + namespace StevesBot.Webhook.Tests.Unit; public class VerifySubscriptionHandlerTests @@ -5,6 +7,7 @@ public class VerifySubscriptionHandlerTests private readonly Mock> _mockSubOptions = new(); private readonly Mock> _mockLogger = new(); private readonly ConcurrentQueue _subscriptionQueue = new(); + private readonly Mock _mockTimeProvider = new(); [Fact] public void Handle_WhenModeIsDenied_ItShouldReturnBadRequest() @@ -14,6 +17,95 @@ public class VerifySubscriptionHandlerTests result.Should().BeOfType>(); } + [Fact] + public void Handle_WhenTopicDoesNotMatch_ItShouldReturnNotFound() + { + SetupSubscriptionOptions(new() { TopicUrl = "expected-topic" }); + + var result = Handle("subscribe", "wrong-topic"); + + result.Should().BeOfType(); + } + + [Fact] + public void Handle_WhenModeIsSubscribeAndLeaseSecondsIsNotPresent_ItShouldReturnBadRequest() + { + var topic = "topic"; + + SetupSubscriptionOptions(new() { TopicUrl = topic }); + + var result = Handle("subscribe", topic, leaseSeconds: null); + + result.Should().BeOfType>(); + } + + [Fact] + public void Handle_WhenModeIsSubscribeAndLeaseSecondsIsInvalid_ItShouldReturnBadRequest() + { + var topic = "topic"; + + SetupSubscriptionOptions(new() { TopicUrl = topic }); + + var result = Handle("subscribe", topic, leaseSeconds: "invalid"); + + result.Should().BeOfType>(); + } + + [Fact] + public void Handle_WhenModeIsUnsubscribe_ItShouldReturnOk() + { + var topic = "topic"; + + SetupSubscriptionOptions(new() { TopicUrl = topic }); + + var result = Handle("unsubscribe", topic, challenge: "challenge"); + + result.Should().BeOfType(); + } + + [Fact] + public void Handle_WhenModeIsSubscribe_ItShouldEnqueueSubscriptionTask() + { + var challenge = "challenge"; + var topic = "topic"; + var leaseSeconds = 3600; + var callbackUrl = "https://example.com/callback"; + var now = DateTime.UtcNow; + + _mockTimeProvider + .Setup(static tp => tp.GetUtcNow()) + .Returns(now); + + SetupSubscriptionOptions(new() { TopicUrl = topic, CallbackUrl = callbackUrl }); + + var result = Handle( + "subscribe", + topic, + challenge: challenge, + leaseSeconds: leaseSeconds.ToString(CultureInfo.InvariantCulture) + ); + + result.Should().BeOfType(); + result.As().ResponseContent.Should().Be(challenge); + + _subscriptionQueue.Count.Should().Be(1); + + var task = _subscriptionQueue.TryDequeue(out var dequeuedTask); + + task.Should().BeTrue(); + dequeuedTask.Should().NotBeNull(); + dequeuedTask!.CallbackUrl.Should().Be(callbackUrl); + dequeuedTask.TopicUrl.Should().Be(topic); + dequeuedTask.ExpiresAt.Should().Be(now.AddSeconds(leaseSeconds)); + } + + private void SetupSubscriptionOptions(SubscriptionOptions options) + { + _mockSubOptions + .Setup(static o => o.Value) + .Returns(options); + } + private IResult Handle(string mode, string topic, string? reason = null, string? challenge = null, string? leaseSeconds = null) { return VerifySubscriptionHandler.Handle( @@ -24,7 +116,8 @@ public class VerifySubscriptionHandlerTests leaseSeconds, _mockSubOptions.Object, _mockLogger.Object, - _subscriptionQueue + _subscriptionQueue, + _mockTimeProvider.Object ); } } \ No newline at end of file