diff --git a/.editorconfig b/.editorconfig
index c58ac87..ee3e4f5 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -110,7 +110,7 @@ csharp_style_prefer_null_check_over_type_check = true : suggestion
csharp_style_implicit_object_creation_when_type_is_apparent = true : suggestion
# primary constructors (C# 12+)
-csharp_style_prefer_primary_constructors = true : suggestion
+csharp_style_prefer_primary_constructors = true : warning
# collection expressions (C# 12+)
dotnet_style_prefer_collection_expression = true : suggestion
diff --git a/src/AzureEventGridSimulator.Tests/Domain/Services/Dashboard/EventHistoryServiceTests.cs b/src/AzureEventGridSimulator.Tests/Domain/Services/Dashboard/EventHistoryServiceTests.cs
index c2a1374..c455dc3 100644
--- a/src/AzureEventGridSimulator.Tests/Domain/Services/Dashboard/EventHistoryServiceTests.cs
+++ b/src/AzureEventGridSimulator.Tests/Domain/Services/Dashboard/EventHistoryServiceTests.cs
@@ -115,7 +115,7 @@ public void RecordDeliveryAttempt_UpdatesDeliveryStatus()
var attempt = new DeliveryAttempt
{
AttemptNumber = 1,
- AttemptTime = DateTime.UtcNow,
+ AttemptTime = DateTimeOffset.UtcNow,
Outcome = DeliveryOutcome.Success,
HttpStatusCode = 200,
};
@@ -136,7 +136,7 @@ public void RecordDeliveryAttempt_NonExistingEvent_DoesNotThrow()
var attempt = new DeliveryAttempt
{
AttemptNumber = 1,
- AttemptTime = DateTime.UtcNow,
+ AttemptTime = DateTimeOffset.UtcNow,
Outcome = DeliveryOutcome.Success,
};
@@ -283,7 +283,7 @@ private static SimulatorEvent CreateTestEvent(string id)
Id = id,
Subject = "/test/subject",
EventType = "Test.EventType",
- EventTime = DateTime.UtcNow.ToString("o"),
+ EventTime = DateTimeOffset.UtcNow.ToString("o"),
DataVersion = "1.0",
Data = new { Property = "Value" },
}
diff --git a/src/AzureEventGridSimulator.Tests/Domain/Services/Dashboard/EventHistoryStoreTests.cs b/src/AzureEventGridSimulator.Tests/Domain/Services/Dashboard/EventHistoryStoreTests.cs
index c67bb51..9a2825c 100644
--- a/src/AzureEventGridSimulator.Tests/Domain/Services/Dashboard/EventHistoryStoreTests.cs
+++ b/src/AzureEventGridSimulator.Tests/Domain/Services/Dashboard/EventHistoryStoreTests.cs
@@ -258,7 +258,7 @@ private static EventHistoryRecord CreateTestRecord(string id, string topicName =
EventType = "Test.EventType",
Subject = "/test/subject",
Source = "/test/source",
- EventTime = DateTime.UtcNow.ToString("o"),
+ EventTime = DateTimeOffset.UtcNow.ToString("o"),
InputSchema = EventSchema.EventGridSchema,
PayloadJson = "{}",
};
diff --git a/src/AzureEventGridSimulator.Tests/Helpers/FakeTimeProvider.cs b/src/AzureEventGridSimulator.Tests/Helpers/FakeTimeProvider.cs
new file mode 100644
index 0000000..67f4cbe
--- /dev/null
+++ b/src/AzureEventGridSimulator.Tests/Helpers/FakeTimeProvider.cs
@@ -0,0 +1,31 @@
+namespace AzureEventGridSimulator.Tests.Helpers;
+
+///
+/// A fake TimeProvider for testing time-dependent code.
+/// Allows tests to control the current time for deterministic behavior.
+///
+public class FakeTimeProvider(DateTimeOffset startTime) : TimeProvider
+{
+ private DateTimeOffset _utcNow = startTime;
+
+ public override DateTimeOffset GetUtcNow()
+ {
+ return _utcNow;
+ }
+
+ ///
+ /// Sets the current time to a specific value.
+ ///
+ public void SetUtcNow(DateTimeOffset value)
+ {
+ _utcNow = value;
+ }
+
+ ///
+ /// Advances the current time by the specified duration.
+ ///
+ public void Advance(TimeSpan duration)
+ {
+ _utcNow = _utcNow.Add(duration);
+ }
+}
diff --git a/src/AzureEventGridSimulator.Tests/UnitTests/CloudEvents/CloudEventSchemaFormatterTests.cs b/src/AzureEventGridSimulator.Tests/UnitTests/CloudEvents/CloudEventSchemaFormatterTests.cs
index fa2efbd..5fa3407 100644
--- a/src/AzureEventGridSimulator.Tests/UnitTests/CloudEvents/CloudEventSchemaFormatterTests.cs
+++ b/src/AzureEventGridSimulator.Tests/UnitTests/CloudEvents/CloudEventSchemaFormatterTests.cs
@@ -72,8 +72,8 @@ public void GivenEventGridEvent_WhenSerializedAsCloudEvent_ThenConvertedCorrectl
EventType = "Test.Event.Type",
EventTime = "2025-01-15T10:30:00Z",
Data = new { Property = "Value" },
- Topic = "/test/topic",
};
+ eventGridEvent.SetTopic("/test/topic");
var simulatorEvent = SimulatorEvent.FromEventGridEvent(eventGridEvent);
var json = _formatter.Serialize(simulatorEvent);
diff --git a/src/AzureEventGridSimulator.Tests/UnitTests/CloudEvents/SimulatorEventTests.cs b/src/AzureEventGridSimulator.Tests/UnitTests/CloudEvents/SimulatorEventTests.cs
index f75ef96..ac6824e 100644
--- a/src/AzureEventGridSimulator.Tests/UnitTests/CloudEvents/SimulatorEventTests.cs
+++ b/src/AzureEventGridSimulator.Tests/UnitTests/CloudEvents/SimulatorEventTests.cs
@@ -18,8 +18,8 @@ public void GivenEventGridEvent_WhenWrappedInSimulatorEvent_ThenPropertiesMapped
EventTime = "2025-01-15T10:30:00Z",
Data = new { Property = "Value" },
DataVersion = "1.0",
- Topic = "/test/topic",
};
+ eventGridEvent.SetTopic("/test/topic");
var simulatorEvent = SimulatorEvent.FromEventGridEvent(eventGridEvent);
diff --git a/src/AzureEventGridSimulator.Tests/UnitTests/Commands/SendNotificationEventsToSubscriberCommandHandlerTests.cs b/src/AzureEventGridSimulator.Tests/UnitTests/Commands/SendNotificationEventsToSubscriberCommandHandlerTests.cs
index 5294e4e..a7d3708 100644
--- a/src/AzureEventGridSimulator.Tests/UnitTests/Commands/SendNotificationEventsToSubscriberCommandHandlerTests.cs
+++ b/src/AzureEventGridSimulator.Tests/UnitTests/Commands/SendNotificationEventsToSubscriberCommandHandlerTests.cs
@@ -407,7 +407,7 @@ private static SimulatorEvent CreateTestEventGridEvent(string id)
Id = id,
Subject = "/test/subject",
EventType = "Test.EventType",
- EventTime = DateTime.UtcNow.ToString("o"),
+ EventTime = DateTimeOffset.UtcNow.ToString("o"),
DataVersion = "1.0",
Data = new { Property = "Value" },
}
diff --git a/src/AzureEventGridSimulator.Tests/UnitTests/Commands/ValidateSubscriptionCommandHandlerTests.cs b/src/AzureEventGridSimulator.Tests/UnitTests/Commands/ValidateSubscriptionCommandHandlerTests.cs
index 94cd777..7e4ba7e 100644
--- a/src/AzureEventGridSimulator.Tests/UnitTests/Commands/ValidateSubscriptionCommandHandlerTests.cs
+++ b/src/AzureEventGridSimulator.Tests/UnitTests/Commands/ValidateSubscriptionCommandHandlerTests.cs
@@ -16,7 +16,7 @@ public class ValidateSubscriptionCommandHandlerTests
public ValidateSubscriptionCommandHandlerTests()
{
_logger = Substitute.For>();
- _handler = new ValidateSubscriptionCommandHandler(_logger);
+ _handler = new ValidateSubscriptionCommandHandler(TimeProvider.System, _logger);
}
[Fact]
diff --git a/src/AzureEventGridSimulator.Tests/UnitTests/EventGrid/EventGridSchemaFormatterTests.cs b/src/AzureEventGridSimulator.Tests/UnitTests/EventGrid/EventGridSchemaFormatterTests.cs
index 5fced06..ea3dad5 100644
--- a/src/AzureEventGridSimulator.Tests/UnitTests/EventGrid/EventGridSchemaFormatterTests.cs
+++ b/src/AzureEventGridSimulator.Tests/UnitTests/EventGrid/EventGridSchemaFormatterTests.cs
@@ -9,7 +9,7 @@ namespace AzureEventGridSimulator.Tests.UnitTests.EventGrid;
[Trait("Category", "unit")]
public class EventGridSchemaFormatterTests
{
- private readonly EventGridSchemaFormatter _formatter = new();
+ private readonly EventGridSchemaFormatter _formatter = new(TimeProvider.System);
[Fact]
public void GivenEventGridEvent_WhenSerialized_ThenJsonContainsAllFields()
@@ -22,9 +22,9 @@ public void GivenEventGridEvent_WhenSerialized_ThenJsonContainsAllFields()
EventTime = "2025-01-15T10:30:00Z",
Data = new { Property = "Value" },
DataVersion = "1.0",
- Topic = "/test/topic",
MetadataVersion = "1",
};
+ eventGridEvent.SetTopic("/test/topic");
var simulatorEvent = SimulatorEvent.FromEventGridEvent(eventGridEvent);
var json = _formatter.Serialize(simulatorEvent);
diff --git a/src/AzureEventGridSimulator.Tests/UnitTests/EventGrid/EventGridValidationTests.cs b/src/AzureEventGridSimulator.Tests/UnitTests/EventGrid/EventGridValidationTests.cs
index 44acc3d..0958219 100644
--- a/src/AzureEventGridSimulator.Tests/UnitTests/EventGrid/EventGridValidationTests.cs
+++ b/src/AzureEventGridSimulator.Tests/UnitTests/EventGrid/EventGridValidationTests.cs
@@ -1,3 +1,4 @@
+using System.Text.Json;
using AzureEventGridSimulator.Domain.Entities;
using Shouldly;
using Xunit;
@@ -258,36 +259,55 @@ string metadataVersion
public void GivenEventWithNonNullTopic_WhenValidated_ThenExceptionThrown()
{
// Topic should be null/empty when publishing - it's set by the service
- var eventGridEvent = new EventGridEvent
- {
- Id = "test-id-123",
- Subject = "/test/subject",
- EventType = "Test.EventType",
- EventTime = "2025-01-15T10:30:00Z",
- Topic = "/subscriptions/some/topic",
- };
-
- var exception = Should.Throw(() => eventGridEvent.Validate());
+ // Use JSON deserialization to simulate a publisher sending a Topic value
+ var json = """
+ {
+ "id": "test-id-123",
+ "subject": "/test/subject",
+ "eventType": "Test.EventType",
+ "eventTime": "2025-01-15T10:30:00Z",
+ "topic": "/subscriptions/some/topic"
+ }
+ """;
+ var eventGridEvent = JsonSerializer.Deserialize(json);
+
+ var exception = Should.Throw(() => eventGridEvent!.Validate());
exception.Message.ShouldContain("Topic");
}
- [Theory]
- [InlineData(null)]
- [InlineData("")]
- public void GivenEventWithNullOrEmptyTopic_WhenValidated_ThenNoExceptionThrown(string topic)
+ [Fact]
+ public void GivenEventWithNullTopic_WhenValidated_ThenNoExceptionThrown()
{
+ // Topic defaults to null when not set
var eventGridEvent = new EventGridEvent
{
Id = "test-id-123",
Subject = "/test/subject",
EventType = "Test.EventType",
EventTime = "2025-01-15T10:30:00Z",
- Topic = topic,
};
Should.NotThrow(() => eventGridEvent.Validate());
}
+ [Fact]
+ public void GivenEventWithEmptyTopic_WhenValidated_ThenNoExceptionThrown()
+ {
+ // Use JSON deserialization to set empty Topic
+ var json = """
+ {
+ "id": "test-id-123",
+ "subject": "/test/subject",
+ "eventType": "Test.EventType",
+ "eventTime": "2025-01-15T10:30:00Z",
+ "topic": ""
+ }
+ """;
+ var eventGridEvent = JsonSerializer.Deserialize(json);
+
+ Should.NotThrow(() => eventGridEvent!.Validate());
+ }
+
[Fact]
public void GivenEventWithNullData_WhenValidated_ThenNoExceptionThrown()
{
diff --git a/src/AzureEventGridSimulator.Tests/UnitTests/Filtering/AdvancedFilterEventAcceptanceTests.cs b/src/AzureEventGridSimulator.Tests/UnitTests/Filtering/AdvancedFilterEventAcceptanceTests.cs
index 80c1af3..a31a2a4 100644
--- a/src/AzureEventGridSimulator.Tests/UnitTests/Filtering/AdvancedFilterEventAcceptanceTests.cs
+++ b/src/AzureEventGridSimulator.Tests/UnitTests/Filtering/AdvancedFilterEventAcceptanceTests.cs
@@ -9,25 +9,31 @@ namespace AzureEventGridSimulator.Tests.UnitTests.Filtering;
[Trait("Category", "unit")]
public class AdvancedFilterEventAcceptanceTests
{
- private static readonly EventGridEvent _gridEvent = new()
+ private static readonly EventGridEvent _gridEvent = CreateTestEvent();
+
+ private static EventGridEvent CreateTestEvent()
{
- Id = "EventId",
- Data = new
+ var evt = new EventGridEvent
{
- NumberValue = 1,
- IsTrue = true,
- Name = "StringValue",
- DoubleValue = 0.12345d,
- NumberMaxValue = ulong.MaxValue,
- SubObject = new { Id = 1, Name = "Test" },
- },
- DataVersion = "5.0",
- EventTime = DateTime.UtcNow.ToString("O"),
- EventType = "this.is.a.test.event.type",
- MetadataVersion = "2.3.4",
- Subject = "TheEventSubject",
- Topic = "THE_EVENT_TOPIC",
- };
+ Id = "EventId",
+ Data = new
+ {
+ NumberValue = 1,
+ IsTrue = true,
+ Name = "StringValue",
+ DoubleValue = 0.12345d,
+ NumberMaxValue = ulong.MaxValue,
+ SubObject = new { Id = 1, Name = "Test" },
+ },
+ DataVersion = "5.0",
+ EventTime = DateTimeOffset.UtcNow.ToString("O"),
+ EventType = "this.is.a.test.event.type",
+ MetadataVersion = "2.3.4",
+ Subject = "TheEventSubject",
+ };
+ evt.SetTopic("THE_EVENT_TOPIC");
+ return evt;
+ }
[Theory]
[ClassData(typeof(PositiveFilterTestCaseContainer))]
diff --git a/src/AzureEventGridSimulator.Tests/UnitTests/Middleware/SasKeyValidatorAegSasTokenTests.cs b/src/AzureEventGridSimulator.Tests/UnitTests/Middleware/SasKeyValidatorAegSasTokenTests.cs
index ddd3ee5..76c9a9b 100644
--- a/src/AzureEventGridSimulator.Tests/UnitTests/Middleware/SasKeyValidatorAegSasTokenTests.cs
+++ b/src/AzureEventGridSimulator.Tests/UnitTests/Middleware/SasKeyValidatorAegSasTokenTests.cs
@@ -14,7 +14,7 @@ public void GivenValidAegSasToken_WhenValidated_ThenReturnsTrue()
var token = GenerateValidSasToken(
ValidTopicKey,
"http://localhost",
- DateTime.UtcNow.AddMinutes(5)
+ DateTimeOffset.UtcNow.AddMinutes(5)
);
var headers = new HeaderDictionary { { Constants.AegSasTokenHeader, token } };
@@ -29,7 +29,7 @@ public void GivenExpiredAegSasToken_WhenValidated_ThenReturnsFalse()
var token = GenerateValidSasToken(
ValidTopicKey,
"http://localhost",
- DateTime.UtcNow.AddMinutes(-5)
+ DateTimeOffset.UtcNow.AddMinutes(-5)
);
var headers = new HeaderDictionary { { Constants.AegSasTokenHeader, token } };
@@ -44,7 +44,7 @@ public void GivenAegSasTokenWithWrongSignature_WhenValidated_ThenReturnsFalse()
var token = GenerateValidSasToken(
"WrongKey123456789012345=",
"http://localhost",
- DateTime.UtcNow.AddMinutes(5)
+ DateTimeOffset.UtcNow.AddMinutes(5)
);
var headers = new HeaderDictionary { { Constants.AegSasTokenHeader, token } };
@@ -59,7 +59,7 @@ public void GivenInvalidAegSasToken_WhenValidated_ThenLogsError()
var token = GenerateValidSasToken(
"WrongKey123456789012345=",
"http://localhost",
- DateTime.UtcNow.AddMinutes(5)
+ DateTimeOffset.UtcNow.AddMinutes(5)
);
var headers = new HeaderDictionary { { Constants.AegSasTokenHeader, token } };
@@ -82,7 +82,7 @@ public void GivenAegSasTokenWithDifferentCase_WhenValidated_ThenHeaderIsFoundCas
var token = GenerateValidSasToken(
ValidTopicKey,
"http://localhost",
- DateTime.UtcNow.AddMinutes(5)
+ DateTimeOffset.UtcNow.AddMinutes(5)
);
var headers = new HeaderDictionary { { "AEG-SAS-TOKEN", token } };
@@ -94,7 +94,7 @@ public void GivenAegSasTokenWithDifferentCase_WhenValidated_ThenHeaderIsFoundCas
[Fact]
public void GivenTokenExpiringExactlyNow_WhenValidated_ThenReturnsFalse()
{
- var token = GenerateValidSasToken(ValidTopicKey, "http://localhost", DateTime.UtcNow);
+ var token = GenerateValidSasToken(ValidTopicKey, "http://localhost", DateTimeOffset.UtcNow);
var headers = new HeaderDictionary { { Constants.AegSasTokenHeader, token } };
var result = Validator.IsValid(headers, ValidTopicKey);
diff --git a/src/AzureEventGridSimulator.Tests/UnitTests/Middleware/SasKeyValidatorAuthorizationHeaderTests.cs b/src/AzureEventGridSimulator.Tests/UnitTests/Middleware/SasKeyValidatorAuthorizationHeaderTests.cs
index 2bdfc12..eb25f5f 100644
--- a/src/AzureEventGridSimulator.Tests/UnitTests/Middleware/SasKeyValidatorAuthorizationHeaderTests.cs
+++ b/src/AzureEventGridSimulator.Tests/UnitTests/Middleware/SasKeyValidatorAuthorizationHeaderTests.cs
@@ -15,7 +15,7 @@ public void GivenValidAuthorizationHeader_WhenValidated_ThenReturnsTrue()
var token = GenerateValidSasToken(
ValidTopicKey,
"http://localhost",
- DateTime.UtcNow.AddMinutes(5)
+ DateTimeOffset.UtcNow.AddMinutes(5)
);
var headers = new HeaderDictionary
{
@@ -33,7 +33,7 @@ public void GivenExpiredAuthorizationHeader_WhenValidated_ThenReturnsFalse()
var token = GenerateValidSasToken(
ValidTopicKey,
"http://localhost",
- DateTime.UtcNow.AddMinutes(-5)
+ DateTimeOffset.UtcNow.AddMinutes(-5)
);
var headers = new HeaderDictionary
{
@@ -51,7 +51,7 @@ public void GivenAuthorizationHeaderWithWrongSignature_WhenValidated_ThenReturns
var token = GenerateValidSasToken(
"WrongKey123456789012345=",
"http://localhost",
- DateTime.UtcNow.AddMinutes(5)
+ DateTimeOffset.UtcNow.AddMinutes(5)
);
var headers = new HeaderDictionary
{
@@ -69,7 +69,7 @@ public void GivenInvalidAuthorizationHeader_WhenValidated_ThenLogsError()
var token = GenerateValidSasToken(
"WrongKey123456789012345=",
"http://localhost",
- DateTime.UtcNow.AddMinutes(5)
+ DateTimeOffset.UtcNow.AddMinutes(5)
);
var headers = new HeaderDictionary
{
@@ -108,7 +108,7 @@ public void GivenAuthorizationHeaderWithDifferentCase_WhenValidated_ThenHeaderIs
var token = GenerateValidSasToken(
ValidTopicKey,
"http://localhost",
- DateTime.UtcNow.AddMinutes(5)
+ DateTimeOffset.UtcNow.AddMinutes(5)
);
var headers = new HeaderDictionary
{
diff --git a/src/AzureEventGridSimulator.Tests/UnitTests/Middleware/SasKeyValidatorTestBase.cs b/src/AzureEventGridSimulator.Tests/UnitTests/Middleware/SasKeyValidatorTestBase.cs
index 8525cd8..91c9650 100644
--- a/src/AzureEventGridSimulator.Tests/UnitTests/Middleware/SasKeyValidatorTestBase.cs
+++ b/src/AzureEventGridSimulator.Tests/UnitTests/Middleware/SasKeyValidatorTestBase.cs
@@ -15,12 +15,16 @@ public abstract class SasKeyValidatorTestBase
protected SasKeyValidatorTestBase()
{
Logger = Substitute.For>();
- Validator = new SasKeyValidator(Logger);
+ Validator = new SasKeyValidator(TimeProvider.System, Logger);
}
- protected static string GenerateValidSasToken(string key, string resource, DateTime expiry)
+ protected static string GenerateValidSasToken(
+ string key,
+ string resource,
+ DateTimeOffset expiry
+ )
{
- var decodedExpiration = expiry.ToString("o");
+ var decodedExpiration = expiry.UtcDateTime.ToString("o");
var encodedResource = HttpUtility.UrlEncode(resource);
var encodedExpiration = HttpUtility.UrlEncode(decodedExpiration);
diff --git a/src/AzureEventGridSimulator.Tests/UnitTests/Retry/DeadLetterServiceTests.cs b/src/AzureEventGridSimulator.Tests/UnitTests/Retry/DeadLetterServiceTests.cs
index afa57bf..2a3a294 100644
--- a/src/AzureEventGridSimulator.Tests/UnitTests/Retry/DeadLetterServiceTests.cs
+++ b/src/AzureEventGridSimulator.Tests/UnitTests/Retry/DeadLetterServiceTests.cs
@@ -68,7 +68,7 @@ private static PendingDelivery CreatePendingDelivery(
Id = eventId ?? Guid.NewGuid().ToString(),
Subject = "test/subject",
EventType = "Test.EventType",
- EventTime = DateTime.UtcNow.ToString("o"),
+ EventTime = DateTimeOffset.UtcNow.ToString("o"),
DataVersion = "1.0",
Data = new { test = "data" },
}
@@ -130,7 +130,7 @@ public async Task GivenDelivery_WhenWriting_ThenFileContainsCorrectJson()
new DeliveryAttempt
{
AttemptNumber = 5,
- AttemptTime = DateTime.UtcNow,
+ AttemptTime = DateTimeOffset.UtcNow,
Outcome = DeliveryOutcome.HttpError,
HttpStatusCode = 503,
ErrorMessage = "Service Unavailable",
diff --git a/src/AzureEventGridSimulator.Tests/UnitTests/Retry/HttpEventDeliveryServiceTests.cs b/src/AzureEventGridSimulator.Tests/UnitTests/Retry/HttpEventDeliveryServiceTests.cs
index 2752140..8eea3fd 100644
--- a/src/AzureEventGridSimulator.Tests/UnitTests/Retry/HttpEventDeliveryServiceTests.cs
+++ b/src/AzureEventGridSimulator.Tests/UnitTests/Retry/HttpEventDeliveryServiceTests.cs
@@ -23,7 +23,7 @@ public HttpEventDeliveryServiceTests()
{
_logger = Substitute.For>();
_formatterFactory = new EventSchemaFormatterFactory(
- new EventGridSchemaFormatter(),
+ new EventGridSchemaFormatter(TimeProvider.System),
new CloudEventSchemaFormatter()
);
}
@@ -264,7 +264,7 @@ private static SimulatorEvent CreateTestEvent()
Id = Guid.NewGuid().ToString(),
Subject = "test/subject",
EventType = "Test.EventType",
- EventTime = DateTime.UtcNow.ToString("o"),
+ EventTime = DateTimeOffset.UtcNow.ToString("o"),
DataVersion = "1.0",
Data = new { test = "data" },
}
diff --git a/src/AzureEventGridSimulator.Tests/UnitTests/Retry/InMemoryDeliveryQueueTests.cs b/src/AzureEventGridSimulator.Tests/UnitTests/Retry/InMemoryDeliveryQueueTests.cs
index 3750ccc..ea31629 100644
--- a/src/AzureEventGridSimulator.Tests/UnitTests/Retry/InMemoryDeliveryQueueTests.cs
+++ b/src/AzureEventGridSimulator.Tests/UnitTests/Retry/InMemoryDeliveryQueueTests.cs
@@ -2,6 +2,7 @@
using AzureEventGridSimulator.Domain.Services.Retry;
using AzureEventGridSimulator.Infrastructure.Settings;
using AzureEventGridSimulator.Infrastructure.Settings.Subscribers;
+using AzureEventGridSimulator.Tests.Helpers;
using NSubstitute;
using Shouldly;
using Xunit;
@@ -11,16 +12,18 @@ namespace AzureEventGridSimulator.Tests.UnitTests.Retry;
[Trait("Category", "unit")]
public class InMemoryDeliveryQueueTests
{
+ private static readonly DateTimeOffset FixedTime = new(2025, 1, 15, 12, 0, 0, TimeSpan.Zero);
private readonly ILogger _logger;
private readonly InMemoryDeliveryQueue _queue;
+ private readonly FakeTimeProvider _timeProvider = new(FixedTime);
public InMemoryDeliveryQueueTests()
{
_logger = Substitute.For>();
- _queue = new InMemoryDeliveryQueue(_logger);
+ _queue = new InMemoryDeliveryQueue(_timeProvider, _logger);
}
- private static PendingDelivery CreatePendingDelivery()
+ private static PendingDelivery CreatePendingDelivery(DateTimeOffset? nextAttemptTime = null)
{
var subscriber = new HttpSubscriberSettings
{
@@ -43,7 +46,7 @@ private static PendingDelivery CreatePendingDelivery()
Id = Guid.NewGuid().ToString(),
Subject = "test/subject",
EventType = "Test.EventType",
- EventTime = DateTime.UtcNow.ToString("o"),
+ EventTime = FixedTime.ToString("o"),
DataVersion = "1.0",
Data = new { test = "data" },
}
@@ -55,6 +58,8 @@ private static PendingDelivery CreatePendingDelivery()
Subscriber = subscriber,
Topic = topic,
InputSchema = EventSchema.EventGridSchema,
+ EnqueuedTime = FixedTime,
+ NextAttemptTime = nextAttemptTime ?? FixedTime,
};
}
@@ -131,7 +136,7 @@ public void GivenExistingDelivery_WhenRequeuing_ThenUpdatesDelivery()
var delivery = CreatePendingDelivery();
_queue.Enqueue(delivery);
- var newNextAttemptTime = DateTime.UtcNow.AddMinutes(5);
+ var newNextAttemptTime = FixedTime.AddMinutes(5);
delivery.NextAttemptTime = newNextAttemptTime;
delivery.AttemptCount = 2;
@@ -163,8 +168,7 @@ public void GivenNoDeliveries_WhenGettingDueDeliveries_ThenReturnsEmpty()
[Fact]
public void GivenDeliveryDueNow_WhenGettingDueDeliveries_ThenReturnsDelivery()
{
- var delivery = CreatePendingDelivery();
- delivery.NextAttemptTime = DateTime.UtcNow.AddSeconds(-1); // Due 1 second ago
+ var delivery = CreatePendingDelivery(FixedTime.AddSeconds(-1));
_queue.Enqueue(delivery);
var dueDeliveries = _queue.GetDueDeliveries().ToList();
@@ -176,8 +180,7 @@ public void GivenDeliveryDueNow_WhenGettingDueDeliveries_ThenReturnsDelivery()
[Fact]
public void GivenDeliveryNotYetDue_WhenGettingDueDeliveries_ThenReturnsEmpty()
{
- var delivery = CreatePendingDelivery();
- delivery.NextAttemptTime = DateTime.UtcNow.AddMinutes(5); // Due in 5 minutes
+ var delivery = CreatePendingDelivery(FixedTime.AddMinutes(5));
_queue.Enqueue(delivery);
var dueDeliveries = _queue.GetDueDeliveries().ToList();
@@ -188,14 +191,9 @@ public void GivenDeliveryNotYetDue_WhenGettingDueDeliveries_ThenReturnsEmpty()
[Fact]
public void GivenMixedDueAndNotDue_WhenGettingDueDeliveries_ThenReturnsOnlyDue()
{
- var dueDelivery1 = CreatePendingDelivery();
- dueDelivery1.NextAttemptTime = DateTime.UtcNow.AddSeconds(-10);
-
- var dueDelivery2 = CreatePendingDelivery();
- dueDelivery2.NextAttemptTime = DateTime.UtcNow.AddSeconds(-5);
-
- var notDueDelivery = CreatePendingDelivery();
- notDueDelivery.NextAttemptTime = DateTime.UtcNow.AddMinutes(5);
+ var dueDelivery1 = CreatePendingDelivery(FixedTime.AddSeconds(-10));
+ var dueDelivery2 = CreatePendingDelivery(FixedTime.AddSeconds(-5));
+ var notDueDelivery = CreatePendingDelivery(FixedTime.AddMinutes(5));
_queue.Enqueue(dueDelivery1);
_queue.Enqueue(dueDelivery2);
@@ -212,14 +210,9 @@ public void GivenMixedDueAndNotDue_WhenGettingDueDeliveries_ThenReturnsOnlyDue()
[Fact]
public void GivenMultipleDueDeliveries_WhenGettingDueDeliveries_ThenOrderedByNextAttemptTime()
{
- var delivery1 = CreatePendingDelivery();
- delivery1.NextAttemptTime = DateTime.UtcNow.AddSeconds(-5);
-
- var delivery2 = CreatePendingDelivery();
- delivery2.NextAttemptTime = DateTime.UtcNow.AddSeconds(-10); // Earlier
-
- var delivery3 = CreatePendingDelivery();
- delivery3.NextAttemptTime = DateTime.UtcNow.AddSeconds(-1); // Latest
+ var delivery1 = CreatePendingDelivery(FixedTime.AddSeconds(-5));
+ var delivery2 = CreatePendingDelivery(FixedTime.AddSeconds(-10)); // Earliest
+ var delivery3 = CreatePendingDelivery(FixedTime.AddSeconds(-1)); // Latest
_queue.Enqueue(delivery1);
_queue.Enqueue(delivery2);
@@ -232,4 +225,22 @@ public void GivenMultipleDueDeliveries_WhenGettingDueDeliveries_ThenOrderedByNex
dueDeliveries[1].Id.ShouldBe(delivery1.Id);
dueDeliveries[2].Id.ShouldBe(delivery3.Id); // Latest last
}
+
+ [Fact]
+ public void GivenDeliveryInFuture_WhenTimeAdvances_ThenBecomesDue()
+ {
+ var delivery = CreatePendingDelivery(FixedTime.AddMinutes(5));
+ _queue.Enqueue(delivery);
+
+ // Initially not due
+ _queue.GetDueDeliveries().ShouldBeEmpty();
+
+ // Advance time by 6 minutes
+ _timeProvider.Advance(TimeSpan.FromMinutes(6));
+
+ // Now should be due
+ var dueDeliveries = _queue.GetDueDeliveries().ToList();
+ dueDeliveries.Count.ShouldBe(1);
+ dueDeliveries[0].Id.ShouldBe(delivery.Id);
+ }
}
diff --git a/src/AzureEventGridSimulator.Tests/UnitTests/Retry/PendingDeliveryTests.cs b/src/AzureEventGridSimulator.Tests/UnitTests/Retry/PendingDeliveryTests.cs
index aada4bc..f3d83bd 100644
--- a/src/AzureEventGridSimulator.Tests/UnitTests/Retry/PendingDeliveryTests.cs
+++ b/src/AzureEventGridSimulator.Tests/UnitTests/Retry/PendingDeliveryTests.cs
@@ -9,11 +9,13 @@ namespace AzureEventGridSimulator.Tests.UnitTests.Retry;
[Trait("Category", "unit")]
public class PendingDeliveryTests
{
+ private static readonly DateTimeOffset FixedTime = new(2025, 1, 15, 12, 0, 0, TimeSpan.Zero);
+
private static PendingDelivery CreatePendingDelivery(
int? ttlMinutes = 1440,
int? maxAttempts = 30,
bool? retryEnabled = true,
- DateTime? enqueuedTime = null
+ DateTimeOffset? enqueuedTime = null
)
{
RetryPolicySettings retryPolicy = null;
@@ -50,59 +52,46 @@ private static PendingDelivery CreatePendingDelivery(
Id = Guid.NewGuid().ToString(),
Subject = "test/subject",
EventType = "Test.EventType",
- EventTime = DateTime.UtcNow.ToString("o"),
+ EventTime = FixedTime.ToString("o"),
DataVersion = "1.0",
Data = new { test = "data" },
}
);
- var delivery = new PendingDelivery
+ return new PendingDelivery
{
Event = evt,
Subscriber = subscriber,
Topic = topic,
InputSchema = EventSchema.EventGridSchema,
+ EnqueuedTime = enqueuedTime ?? FixedTime,
+ NextAttemptTime = enqueuedTime ?? FixedTime,
};
-
- // Set EnqueuedTime if provided (using object initializer since it's init-only)
- if (enqueuedTime.HasValue)
- {
- return new PendingDelivery
- {
- Event = evt,
- Subscriber = subscriber,
- Topic = topic,
- InputSchema = EventSchema.EventGridSchema,
- EnqueuedTime = enqueuedTime.Value,
- };
- }
-
- return delivery;
}
[Fact]
public void GivenNewDelivery_WhenChecking_ThenIsNotExpired()
{
- var delivery = CreatePendingDelivery(60);
+ var delivery = CreatePendingDelivery(60, enqueuedTime: FixedTime);
- delivery.IsExpired.ShouldBeFalse();
+ delivery.IsExpired(FixedTime).ShouldBeFalse();
}
[Fact]
public void GivenDeliveryOlderThanTtl_WhenChecking_ThenIsExpired()
{
- var delivery = CreatePendingDelivery(60, enqueuedTime: DateTime.UtcNow.AddMinutes(-61));
+ var delivery = CreatePendingDelivery(60, enqueuedTime: FixedTime.AddMinutes(-61));
- delivery.IsExpired.ShouldBeTrue();
+ delivery.IsExpired(FixedTime).ShouldBeTrue();
}
[Fact]
public void GivenDeliveryJustBeforeTtl_WhenChecking_ThenIsNotExpired()
{
- var delivery = CreatePendingDelivery(60, enqueuedTime: DateTime.UtcNow.AddMinutes(-59));
+ var delivery = CreatePendingDelivery(60, enqueuedTime: FixedTime.AddMinutes(-59));
// Just before TTL, should not be expired yet
- delivery.IsExpired.ShouldBeFalse();
+ delivery.IsExpired(FixedTime).ShouldBeFalse();
}
[Fact]
@@ -111,10 +100,10 @@ public void GivenDeliveryWithDefaultTtl_WhenChecking_ThenExpiredAfter24Hours()
// Default TTL is 1440 minutes (24 hours)
var delivery = CreatePendingDelivery(
null, // Use default
- enqueuedTime: DateTime.UtcNow.AddMinutes(-1441)
+ enqueuedTime: FixedTime.AddMinutes(-1441)
);
- delivery.IsExpired.ShouldBeTrue();
+ delivery.IsExpired(FixedTime).ShouldBeTrue();
}
[Fact]
@@ -122,10 +111,10 @@ public void GivenDeliveryWithShortTtl_WhenChecking_ThenExpiredQuickly()
{
var delivery = CreatePendingDelivery(
1, // 1 minute TTL
- enqueuedTime: DateTime.UtcNow.AddMinutes(-2)
+ enqueuedTime: FixedTime.AddMinutes(-2)
);
- delivery.IsExpired.ShouldBeTrue();
+ delivery.IsExpired(FixedTime).ShouldBeTrue();
}
[Fact]
@@ -225,7 +214,7 @@ public void GivenOneAttempt_WhenGettingLastAttempt_ThenReturnsThatAttempt()
var attempt = new DeliveryAttempt
{
AttemptNumber = 1,
- AttemptTime = DateTime.UtcNow,
+ AttemptTime = FixedTime,
Outcome = DeliveryOutcome.HttpError,
HttpStatusCode = 500,
};
@@ -241,20 +230,20 @@ public void GivenMultipleAttempts_WhenGettingLastAttempt_ThenReturnsLast()
var attempt1 = new DeliveryAttempt
{
AttemptNumber = 1,
- AttemptTime = DateTime.UtcNow.AddMinutes(-10),
+ AttemptTime = FixedTime.AddMinutes(-10),
Outcome = DeliveryOutcome.HttpError,
HttpStatusCode = 500,
};
var attempt2 = new DeliveryAttempt
{
AttemptNumber = 2,
- AttemptTime = DateTime.UtcNow.AddMinutes(-5),
+ AttemptTime = FixedTime.AddMinutes(-5),
Outcome = DeliveryOutcome.Timeout,
};
var attempt3 = new DeliveryAttempt
{
AttemptNumber = 3,
- AttemptTime = DateTime.UtcNow,
+ AttemptTime = FixedTime,
Outcome = DeliveryOutcome.HttpError,
HttpStatusCode = 503,
};
@@ -283,13 +272,7 @@ public void GivenNewDelivery_WhenCreated_ThenHasCorrectDefaults()
delivery.AttemptCount.ShouldBe(0);
delivery.Attempts.ShouldBeEmpty();
- delivery.EnqueuedTime.ShouldBeInRange(
- DateTime.UtcNow.AddSeconds(-1),
- DateTime.UtcNow.AddSeconds(1)
- );
- delivery.NextAttemptTime.ShouldBeInRange(
- DateTime.UtcNow.AddSeconds(-1),
- DateTime.UtcNow.AddSeconds(1)
- );
+ delivery.EnqueuedTime.ShouldBe(FixedTime);
+ delivery.NextAttemptTime.ShouldBe(FixedTime);
}
}
diff --git a/src/AzureEventGridSimulator.Tests/UnitTests/Retry/RetrySchedulerTests.cs b/src/AzureEventGridSimulator.Tests/UnitTests/Retry/RetrySchedulerTests.cs
index 742f2f3..5a454e6 100644
--- a/src/AzureEventGridSimulator.Tests/UnitTests/Retry/RetrySchedulerTests.cs
+++ b/src/AzureEventGridSimulator.Tests/UnitTests/Retry/RetrySchedulerTests.cs
@@ -1,4 +1,5 @@
using AzureEventGridSimulator.Domain.Services.Retry;
+using AzureEventGridSimulator.Tests.Helpers;
using Shouldly;
using Xunit;
@@ -7,6 +8,15 @@ namespace AzureEventGridSimulator.Tests.UnitTests.Retry;
[Trait("Category", "unit")]
public class RetrySchedulerTests
{
+ private static readonly DateTimeOffset FixedTime = new(2025, 1, 15, 12, 0, 0, TimeSpan.Zero);
+ private readonly RetryScheduler _scheduler;
+ private readonly FakeTimeProvider _timeProvider = new(FixedTime);
+
+ public RetrySchedulerTests()
+ {
+ _scheduler = new RetryScheduler(_timeProvider);
+ }
+
[Theory]
[InlineData(1, 10)] // 10 seconds
[InlineData(2, 30)] // 30 seconds
@@ -23,16 +33,10 @@ public void GivenAttemptNumber_WhenGettingNextRetryTime_ThenFollowsAzureSchedule
int expectedDelaySeconds
)
{
- var before = DateTime.UtcNow;
+ var nextRetryTime = _scheduler.GetNextRetryTime(attemptNumber);
- var nextRetryTime = RetryScheduler.GetNextRetryTime(attemptNumber);
-
- var after = DateTime.UtcNow;
- var expectedMin = before.AddSeconds(expectedDelaySeconds);
- var expectedMax = after.AddSeconds(expectedDelaySeconds + 1); // Allow 1 second tolerance
-
- nextRetryTime.ShouldBeGreaterThanOrEqualTo(expectedMin);
- nextRetryTime.ShouldBeLessThanOrEqualTo(expectedMax);
+ var expectedTime = FixedTime.AddSeconds(expectedDelaySeconds);
+ nextRetryTime.ShouldBe(expectedTime);
}
[Theory]
@@ -44,17 +48,12 @@ public void GivenAttemptBeyondSchedule_WhenGettingNextRetryTime_ThenReturns12Hou
int attemptNumber
)
{
- var before = DateTime.UtcNow;
var expectedDelaySeconds = 12 * 60 * 60; // 12 hours
- var nextRetryTime = RetryScheduler.GetNextRetryTime(attemptNumber);
+ var nextRetryTime = _scheduler.GetNextRetryTime(attemptNumber);
- var after = DateTime.UtcNow;
- var expectedMin = before.AddSeconds(expectedDelaySeconds);
- var expectedMax = after.AddSeconds(expectedDelaySeconds + 1);
-
- nextRetryTime.ShouldBeGreaterThanOrEqualTo(expectedMin);
- nextRetryTime.ShouldBeLessThanOrEqualTo(expectedMax);
+ var expectedTime = FixedTime.AddSeconds(expectedDelaySeconds);
+ nextRetryTime.ShouldBe(expectedTime);
}
[Theory]
@@ -64,59 +63,57 @@ public void GivenInvalidAttemptNumber_WhenGettingNextRetryTime_ThenReturnsImmedi
int attemptNumber
)
{
- var before = DateTime.UtcNow;
-
- var nextRetryTime = RetryScheduler.GetNextRetryTime(attemptNumber);
+ var nextRetryTime = _scheduler.GetNextRetryTime(attemptNumber);
- nextRetryTime.ShouldBeLessThanOrEqualTo(before.AddSeconds(1));
+ nextRetryTime.ShouldBe(FixedTime);
}
[Fact]
public void GivenHttp404_WhenGettingNextRetryTime_ThenMinimum5MinuteDelay()
{
- var before = DateTime.UtcNow;
var expectedMinDelaySeconds = 5 * 60; // 5 minutes
// First attempt would normally be 10s, but 404 requires minimum 5 minutes
- var nextRetryTime = RetryScheduler.GetNextRetryTime(1, 404);
+ var nextRetryTime = _scheduler.GetNextRetryTime(1, 404);
- nextRetryTime.ShouldBeGreaterThanOrEqualTo(before.AddSeconds(expectedMinDelaySeconds));
+ var expectedTime = FixedTime.AddSeconds(expectedMinDelaySeconds);
+ nextRetryTime.ShouldBe(expectedTime);
}
[Fact]
public void GivenHttp408_WhenGettingNextRetryTime_ThenMinimum2MinuteDelay()
{
- var before = DateTime.UtcNow;
var expectedMinDelaySeconds = 2 * 60; // 2 minutes
// First attempt would normally be 10s, but 408 requires minimum 2 minutes
- var nextRetryTime = RetryScheduler.GetNextRetryTime(1, 408);
+ var nextRetryTime = _scheduler.GetNextRetryTime(1, 408);
- nextRetryTime.ShouldBeGreaterThanOrEqualTo(before.AddSeconds(expectedMinDelaySeconds));
+ var expectedTime = FixedTime.AddSeconds(expectedMinDelaySeconds);
+ nextRetryTime.ShouldBe(expectedTime);
}
[Fact]
public void GivenHttp503_WhenGettingNextRetryTime_ThenMinimum30SecondDelay()
{
- var before = DateTime.UtcNow;
var expectedMinDelaySeconds = 30; // 30 seconds
// First attempt would normally be 10s, but 503 requires minimum 30 seconds
- var nextRetryTime = RetryScheduler.GetNextRetryTime(1, 503);
+ var nextRetryTime = _scheduler.GetNextRetryTime(1, 503);
- nextRetryTime.ShouldBeGreaterThanOrEqualTo(before.AddSeconds(expectedMinDelaySeconds));
+ var expectedTime = FixedTime.AddSeconds(expectedMinDelaySeconds);
+ nextRetryTime.ShouldBe(expectedTime);
}
[Fact]
public void GivenHttp503OnLaterAttempt_WhenStandardDelayIsLarger_ThenUsesStandardDelay()
{
- var before = DateTime.UtcNow;
var expectedMinDelaySeconds = 5 * 60; // 5 minutes (attempt 4 standard delay)
// Attempt 4 standard delay is 5 minutes, which is larger than 503's 30 seconds
- var nextRetryTime = RetryScheduler.GetNextRetryTime(4, 503);
+ var nextRetryTime = _scheduler.GetNextRetryTime(4, 503);
- nextRetryTime.ShouldBeGreaterThanOrEqualTo(before.AddSeconds(expectedMinDelaySeconds));
+ var expectedTime = FixedTime.AddSeconds(expectedMinDelaySeconds);
+ nextRetryTime.ShouldBe(expectedTime);
}
[Theory]
@@ -127,7 +124,7 @@ public void GivenHttp503OnLaterAttempt_WhenStandardDelayIsLarger_ThenUsesStandar
[InlineData(204)]
public void GivenSuccessStatusCode_WhenChecking_ThenReturnsTrue(int statusCode)
{
- RetryScheduler.IsSuccessStatusCode(statusCode).ShouldBeTrue();
+ _scheduler.IsSuccessStatusCode(statusCode).ShouldBeTrue();
}
[Theory]
@@ -140,7 +137,7 @@ public void GivenSuccessStatusCode_WhenChecking_ThenReturnsTrue(int statusCode)
[InlineData(503)]
public void GivenNonSuccessStatusCode_WhenChecking_ThenReturnsFalse(int statusCode)
{
- RetryScheduler.IsSuccessStatusCode(statusCode).ShouldBeFalse();
+ _scheduler.IsSuccessStatusCode(statusCode).ShouldBeFalse();
}
[Theory]
@@ -150,7 +147,7 @@ public void GivenNonSuccessStatusCode_WhenChecking_ThenReturnsFalse(int statusCo
[InlineData(413)]
public void GivenImmediateDeadLetterStatusCode_WhenChecking_ThenReturnsTrue(int statusCode)
{
- RetryScheduler.ShouldImmediatelyDeadLetter(statusCode).ShouldBeTrue();
+ _scheduler.ShouldImmediatelyDeadLetter(statusCode).ShouldBeTrue();
}
[Theory]
@@ -163,7 +160,7 @@ public void GivenImmediateDeadLetterStatusCode_WhenChecking_ThenReturnsTrue(int
[InlineData(504)]
public void GivenRetryableStatusCode_WhenChecking_ThenReturnsFalse(int statusCode)
{
- RetryScheduler.ShouldImmediatelyDeadLetter(statusCode).ShouldBeFalse();
+ _scheduler.ShouldImmediatelyDeadLetter(statusCode).ShouldBeFalse();
}
[Theory]
@@ -176,7 +173,7 @@ public void GivenKnownStatusCode_WhenGettingDeadLetterReason_ThenReturnsSpecific
string expectedReason
)
{
- RetryScheduler.GetDeadLetterReasonForStatusCode(statusCode).ShouldBe(expectedReason);
+ _scheduler.GetDeadLetterReasonForStatusCode(statusCode).ShouldBe(expectedReason);
}
[Theory]
@@ -188,6 +185,6 @@ public void GivenUnknownStatusCode_WhenGettingDeadLetterReason_ThenReturnsGeneri
string expectedReason
)
{
- RetryScheduler.GetDeadLetterReasonForStatusCode(statusCode).ShouldBe(expectedReason);
+ _scheduler.GetDeadLetterReasonForStatusCode(statusCode).ShouldBe(expectedReason);
}
}
diff --git a/src/AzureEventGridSimulator.Tests/UnitTests/Subscribers/Delivery/EventHubEventDeliveryServiceTests.cs b/src/AzureEventGridSimulator.Tests/UnitTests/Subscribers/Delivery/EventHubEventDeliveryServiceTests.cs
index 9521512..4dbefe3 100644
--- a/src/AzureEventGridSimulator.Tests/UnitTests/Subscribers/Delivery/EventHubEventDeliveryServiceTests.cs
+++ b/src/AzureEventGridSimulator.Tests/UnitTests/Subscribers/Delivery/EventHubEventDeliveryServiceTests.cs
@@ -21,7 +21,7 @@ public EventHubEventDeliveryServiceTests()
{
_logger = Substitute.For>();
_formatterFactory = new EventSchemaFormatterFactory(
- new EventGridSchemaFormatter(),
+ new EventGridSchemaFormatter(TimeProvider.System),
new CloudEventSchemaFormatter()
);
_propertyResolver = new DeliveryPropertyResolver();
diff --git a/src/AzureEventGridSimulator.Tests/UnitTests/Subscribers/Delivery/ServiceBusEventDeliveryServiceTests.cs b/src/AzureEventGridSimulator.Tests/UnitTests/Subscribers/Delivery/ServiceBusEventDeliveryServiceTests.cs
index 5c3beeb..e4c8335 100644
--- a/src/AzureEventGridSimulator.Tests/UnitTests/Subscribers/Delivery/ServiceBusEventDeliveryServiceTests.cs
+++ b/src/AzureEventGridSimulator.Tests/UnitTests/Subscribers/Delivery/ServiceBusEventDeliveryServiceTests.cs
@@ -21,7 +21,7 @@ public ServiceBusEventDeliveryServiceTests()
{
_logger = Substitute.For>();
_formatterFactory = new EventSchemaFormatterFactory(
- new EventGridSchemaFormatter(),
+ new EventGridSchemaFormatter(TimeProvider.System),
new CloudEventSchemaFormatter()
);
_propertyResolver = new DeliveryPropertyResolver();
diff --git a/src/AzureEventGridSimulator.Tests/UnitTests/Subscribers/Delivery/StorageQueueEventDeliveryServiceTests.cs b/src/AzureEventGridSimulator.Tests/UnitTests/Subscribers/Delivery/StorageQueueEventDeliveryServiceTests.cs
index 6f81deb..52350ff 100644
--- a/src/AzureEventGridSimulator.Tests/UnitTests/Subscribers/Delivery/StorageQueueEventDeliveryServiceTests.cs
+++ b/src/AzureEventGridSimulator.Tests/UnitTests/Subscribers/Delivery/StorageQueueEventDeliveryServiceTests.cs
@@ -21,7 +21,7 @@ public StorageQueueEventDeliveryServiceTests()
{
_logger = Substitute.For>();
_formatterFactory = new EventSchemaFormatterFactory(
- new EventGridSchemaFormatter(),
+ new EventGridSchemaFormatter(TimeProvider.System),
new CloudEventSchemaFormatter()
);
_service = new StorageQueueEventDeliveryService(_logger, _formatterFactory);
diff --git a/src/AzureEventGridSimulator/Domain/Commands/SendNotificationEventsToSubscriberCommandHandler.cs b/src/AzureEventGridSimulator/Domain/Commands/SendNotificationEventsToSubscriberCommandHandler.cs
index a6c3442..bd3a8dc 100644
--- a/src/AzureEventGridSimulator/Domain/Commands/SendNotificationEventsToSubscriberCommandHandler.cs
+++ b/src/AzureEventGridSimulator/Domain/Commands/SendNotificationEventsToSubscriberCommandHandler.cs
@@ -164,7 +164,7 @@ private static void EnrichEvents(SimulatorEvent[] events, string topicName)
{
if (evt.Schema == EventSchema.EventGridSchema && evt.EventGridEvent != null)
{
- evt.EventGridEvent.Topic = topicPath;
+ evt.EventGridEvent.SetTopic(topicPath);
evt.EventGridEvent.MetadataVersion = "1";
}
else if (evt.Schema == EventSchema.CloudEventV1_0 && evt.CloudEvent != null)
diff --git a/src/AzureEventGridSimulator/Domain/Commands/ValidateAllSubscriptionsCommandHandler.cs b/src/AzureEventGridSimulator/Domain/Commands/ValidateAllSubscriptionsCommandHandler.cs
index 5d411ff..7ae0a7b 100644
--- a/src/AzureEventGridSimulator/Domain/Commands/ValidateAllSubscriptionsCommandHandler.cs
+++ b/src/AzureEventGridSimulator/Domain/Commands/ValidateAllSubscriptionsCommandHandler.cs
@@ -16,7 +16,8 @@ public class ValidateAllSubscriptionsCommandHandler(
ILogger logger,
IHttpClientFactory httpClientFactory,
SimulatorSettings simulatorSettings,
- ValidationIpAddressProvider validationIpAddress
+ ValidationIpAddressProvider validationIpAddress,
+ TimeProvider timeProvider
) : IRequestHandler
{
public async Task Handle(
@@ -55,7 +56,7 @@ HttpSubscriberSettings subscription
var evt = new EventGridEvent
{
- EventTime = DateTime.UtcNow.ToString("O"),
+ EventTime = timeProvider.GetUtcNow().ToString("o"),
DataVersion = "1",
EventType = "Microsoft.EventGrid.SubscriptionValidationEvent",
Id = Guid.NewGuid().ToString(),
diff --git a/src/AzureEventGridSimulator/Domain/Commands/ValidateSubscriptionCommandHandler.cs b/src/AzureEventGridSimulator/Domain/Commands/ValidateSubscriptionCommandHandler.cs
index 7356e84..2cc8889 100644
--- a/src/AzureEventGridSimulator/Domain/Commands/ValidateSubscriptionCommandHandler.cs
+++ b/src/AzureEventGridSimulator/Domain/Commands/ValidateSubscriptionCommandHandler.cs
@@ -3,8 +3,10 @@
namespace AzureEventGridSimulator.Domain.Commands;
-public class ValidateSubscriptionCommandHandler(ILogger logger)
- : IRequestHandler
+public class ValidateSubscriptionCommandHandler(
+ TimeProvider timeProvider,
+ ILogger logger
+) : IRequestHandler
{
public Task Handle(
ValidateSubscriptionCommand request,
@@ -19,7 +21,7 @@ CancellationToken cancellationToken
if (
subscriber != null
&& subscriber.ValidationCode == request.ValidationCode
- && !subscriber.ValidationPeriodExpired
+ && !subscriber.ValidationPeriodExpired(timeProvider.GetUtcNow())
)
{
subscriber.ValidationStatus = SubscriptionValidationStatus.ValidationSuccessful;
diff --git a/src/AzureEventGridSimulator/Domain/Entities/CloudEvent.cs b/src/AzureEventGridSimulator/Domain/Entities/CloudEvent.cs
index 382a756..af18a15 100644
--- a/src/AzureEventGridSimulator/Domain/Entities/CloudEvent.cs
+++ b/src/AzureEventGridSimulator/Domain/Entities/CloudEvent.cs
@@ -1,3 +1,4 @@
+using System.Globalization;
using System.Text.Json.Serialization;
namespace AzureEventGridSimulator.Domain.Entities;
@@ -76,10 +77,22 @@ public class CloudEvent
public string DataBase64 { get; set; }
[JsonIgnore]
- private DateTime? TimeParsed => string.IsNullOrEmpty(Time) ? null : DateTime.Parse(Time);
+ private DateTimeOffset? TimeParsed =>
+ string.IsNullOrEmpty(Time)
+ ? null
+ : DateTimeOffset.Parse(Time, CultureInfo.InvariantCulture);
[JsonIgnore]
- private bool TimeIsValid => string.IsNullOrEmpty(Time) || DateTime.TryParse(Time, out _);
+ private bool TimeIsValid =>
+ string.IsNullOrEmpty(Time)
+ || DateTimeOffset.TryParse(Time, CultureInfo.InvariantCulture, out _);
+
+ [JsonIgnore]
+ private bool TimeHasTimezone =>
+ string.IsNullOrEmpty(Time)
+ || Time.Contains('Z')
+ || Time.Contains('+')
+ || (Time.Length > 10 && Time[10..].Contains('-'));
///
/// Validate the CloudEvent according to the CloudEvents v1.0 specification.
@@ -141,10 +154,10 @@ public void Validate()
);
}
- if (TimeParsed is { Kind: DateTimeKind.Unspecified })
+ if (!TimeHasTimezone)
{
throw new InvalidOperationException(
- $"Property '{nameof(Time)}' must include timezone information."
+ $"Property '{nameof(Time)}' must include timezone information (e.g., 'Z' for UTC or an offset like '+00:00')."
);
}
}
diff --git a/src/AzureEventGridSimulator/Domain/Entities/DeadLetterEvent.cs b/src/AzureEventGridSimulator/Domain/Entities/DeadLetterEvent.cs
index 0b22cbd..8e2d654 100644
--- a/src/AzureEventGridSimulator/Domain/Entities/DeadLetterEvent.cs
+++ b/src/AzureEventGridSimulator/Domain/Entities/DeadLetterEvent.cs
@@ -44,13 +44,13 @@ public class DeadLetterEvent
/// Gets or sets the time the event was originally published.
///
[JsonPropertyName("publishTime")]
- public DateTime PublishTime { get; init; }
+ public DateTimeOffset PublishTime { get; init; }
///
/// Gets or sets the time of the last delivery attempt.
///
[JsonPropertyName("lastDeliveryAttemptTime")]
- public DateTime LastDeliveryAttemptTime { get; init; }
+ public DateTimeOffset LastDeliveryAttemptTime { get; init; }
///
/// Gets or sets the topic name.
diff --git a/src/AzureEventGridSimulator/Domain/Entities/DeliveryAttempt.cs b/src/AzureEventGridSimulator/Domain/Entities/DeliveryAttempt.cs
index 6eca0d7..ab1da10 100644
--- a/src/AzureEventGridSimulator/Domain/Entities/DeliveryAttempt.cs
+++ b/src/AzureEventGridSimulator/Domain/Entities/DeliveryAttempt.cs
@@ -8,7 +8,7 @@ public class DeliveryAttempt
///
/// Gets or sets the time of the attempt.
///
- public DateTime AttemptTime { get; init; } = DateTime.UtcNow;
+ public DateTimeOffset AttemptTime { get; init; } = DateTimeOffset.UtcNow;
///
/// Gets or sets the attempt number (1-based).
diff --git a/src/AzureEventGridSimulator/Domain/Entities/EventGridEvent.cs b/src/AzureEventGridSimulator/Domain/Entities/EventGridEvent.cs
index f719aa1..7486d68 100644
--- a/src/AzureEventGridSimulator/Domain/Entities/EventGridEvent.cs
+++ b/src/AzureEventGridSimulator/Domain/Entities/EventGridEvent.cs
@@ -1,3 +1,4 @@
+using System.Globalization;
using System.Text.Json.Serialization;
namespace AzureEventGridSimulator.Domain.Entities;
@@ -38,10 +39,18 @@ public class EventGridEvent
public string EventTime { get; set; }
[JsonIgnore]
- private DateTime EventTimeParsed => DateTime.Parse(EventTime);
+ private DateTimeOffset EventTimeParsed =>
+ DateTimeOffset.Parse(EventTime, CultureInfo.InvariantCulture);
[JsonIgnore]
- private bool EventTimeIsValid => DateTime.TryParse(EventTime, out _);
+ private bool EventTimeIsValid =>
+ DateTimeOffset.TryParse(EventTime, CultureInfo.InvariantCulture, out _);
+
+ [JsonIgnore]
+ private bool EventTimeHasTimezone =>
+ EventTime.Contains('Z')
+ || EventTime.Contains('+')
+ || (EventTime.Length > 10 && EventTime[10..].Contains('-'));
///
/// Gets or sets the schema version of the data object.
@@ -56,10 +65,27 @@ public class EventGridEvent
public string MetadataVersion { get; set; }
///
- /// Gets or sets the resource path of the event source.
+ /// Gets the resource path of the event source.
+ /// This property is set by Event Grid, not by publishers.
///
[JsonPropertyName("topic")]
- public string Topic { get; set; }
+ [JsonInclude]
+ public string Topic { get; private set; }
+
+ ///
+ /// Indicates whether the Topic has been set by the simulator.
+ ///
+ [JsonIgnore]
+ internal bool TopicHasBeenSet { get; private set; }
+
+ ///
+ /// Sets the topic path. This should only be called by the simulator.
+ ///
+ internal void SetTopic(string topic)
+ {
+ Topic = topic;
+ TopicHasBeenSet = true;
+ }
///
/// Validate the object.
@@ -102,10 +128,10 @@ public void Validate()
);
}
- if (EventTimeParsed.Kind == DateTimeKind.Unspecified)
+ if (!EventTimeHasTimezone)
{
throw new InvalidOperationException(
- $"Property '{nameof(EventTime)}' must be either Local or UTC."
+ $"Property '{nameof(EventTime)}' must include a timezone indicator (e.g., 'Z' for UTC or an offset like '+00:00')."
);
}
@@ -116,7 +142,9 @@ public void Validate()
);
}
- if (!string.IsNullOrEmpty(Topic))
+ // Topic must NOT be set by the publisher - Event Grid sets this automatically
+ // Skip this check if the simulator has already set the topic via SetTopic()
+ if (!TopicHasBeenSet && !string.IsNullOrEmpty(Topic))
{
throw new InvalidOperationException(
$"Property '{nameof(Topic)}' was found to be set to '{Topic}', but was expected to either be null/empty."
diff --git a/src/AzureEventGridSimulator/Domain/Entities/PendingDelivery.cs b/src/AzureEventGridSimulator/Domain/Entities/PendingDelivery.cs
index 0bb6c53..b3112b3 100644
--- a/src/AzureEventGridSimulator/Domain/Entities/PendingDelivery.cs
+++ b/src/AzureEventGridSimulator/Domain/Entities/PendingDelivery.cs
@@ -36,12 +36,12 @@ public class PendingDelivery
///
/// Gets or sets the time the event was enqueued.
///
- public DateTime EnqueuedTime { get; init; } = DateTime.UtcNow;
+ public DateTimeOffset EnqueuedTime { get; init; } = DateTimeOffset.UtcNow;
///
/// Gets or sets the next scheduled attempt time.
///
- public DateTime NextAttemptTime { get; set; } = DateTime.UtcNow;
+ public DateTimeOffset NextAttemptTime { get; set; } = DateTimeOffset.UtcNow;
///
/// Gets or sets the number of delivery attempts made.
@@ -59,12 +59,6 @@ public class PendingDelivery
private RetryPolicySettings EffectiveRetryPolicy =>
Subscriber.RetryPolicy ?? new RetryPolicySettings();
- ///
- /// Gets whether the event has expired based on TTL.
- ///
- public bool IsExpired =>
- DateTime.UtcNow > EnqueuedTime.AddMinutes(EffectiveRetryPolicy.EventTimeToLiveInMinutes);
-
///
/// Gets whether the maximum delivery attempts have been reached.
///
@@ -79,4 +73,18 @@ public class PendingDelivery
/// Gets the last delivery attempt, if any.
///
public DeliveryAttempt LastAttempt => Attempts.Count > 0 ? Attempts[^1] : null;
+
+ ///
+ /// Determines whether the event has expired based on TTL.
+ ///
+ ///
+ /// The current UTC time.
+ ///
+ ///
+ /// True if the event has expired.
+ ///
+ public bool IsExpired(DateTimeOffset now)
+ {
+ return now > EnqueuedTime.AddMinutes(EffectiveRetryPolicy.EventTimeToLiveInMinutes);
+ }
}
diff --git a/src/AzureEventGridSimulator/Domain/Services/Delivery/DeliveryPropertyResolver.cs b/src/AzureEventGridSimulator/Domain/Services/Delivery/DeliveryPropertyResolver.cs
index d8ae119..1d4a718 100644
--- a/src/AzureEventGridSimulator/Domain/Services/Delivery/DeliveryPropertyResolver.cs
+++ b/src/AzureEventGridSimulator/Domain/Services/Delivery/DeliveryPropertyResolver.cs
@@ -1,3 +1,4 @@
+using System.Globalization;
using System.Text.Json;
using AzureEventGridSimulator.Domain.Entities;
using AzureEventGridSimulator.Infrastructure.Settings.Subscribers;
@@ -202,7 +203,7 @@ JsonValueKind.Number when element.TryGetInt64(out var l) => l,
};
}
- private static bool TryParseDateTime(string value, out DateTime result)
+ private static bool TryParseDateTime(string value, out DateTimeOffset result)
{
result = default;
if (string.IsNullOrEmpty(value))
@@ -210,7 +211,7 @@ private static bool TryParseDateTime(string value, out DateTime result)
return false;
}
- return DateTime.TryParse(value, out result);
+ return DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, out result);
}
private static bool TryParseGuid(string value, out Guid result)
diff --git a/src/AzureEventGridSimulator/Domain/Services/EventGridSchemaFormatter.cs b/src/AzureEventGridSimulator/Domain/Services/EventGridSchemaFormatter.cs
index e02bc60..1083499 100644
--- a/src/AzureEventGridSimulator/Domain/Services/EventGridSchemaFormatter.cs
+++ b/src/AzureEventGridSimulator/Domain/Services/EventGridSchemaFormatter.cs
@@ -6,7 +6,7 @@ namespace AzureEventGridSimulator.Domain.Services;
///
/// Formats events in the Azure Event Grid schema for delivery.
///
-public class EventGridSchemaFormatter : IEventSchemaFormatter
+public class EventGridSchemaFormatter(TimeProvider timeProvider) : IEventSchemaFormatter
{
///
public EventSchema Schema => EventSchema.EventGridSchema;
@@ -62,17 +62,18 @@ private EventGridEvent ConvertToEventGridEvent(SimulatorEvent evt)
///
private EventGridEvent ConvertCloudEventToEventGrid(CloudEvent cloudEvent)
{
- return new EventGridEvent
+ var eventGridEvent = new EventGridEvent
{
Id = cloudEvent.Id,
Subject = cloudEvent.Subject ?? cloudEvent.Source,
EventType = cloudEvent.Type,
- EventTime = cloudEvent.Time ?? DateTime.UtcNow.ToString("o"),
+ EventTime = cloudEvent.Time ?? timeProvider.GetUtcNow().ToString("o"),
Data = cloudEvent.Data,
DataVersion = ExtractDataVersion(cloudEvent.DataSchema),
- Topic = cloudEvent.Source,
MetadataVersion = "1",
};
+ eventGridEvent.SetTopic(cloudEvent.Source);
+ return eventGridEvent;
}
///
diff --git a/src/AzureEventGridSimulator/Domain/Services/Retry/InMemoryDeliveryQueue.cs b/src/AzureEventGridSimulator/Domain/Services/Retry/InMemoryDeliveryQueue.cs
index 11dc997..82b49b4 100644
--- a/src/AzureEventGridSimulator/Domain/Services/Retry/InMemoryDeliveryQueue.cs
+++ b/src/AzureEventGridSimulator/Domain/Services/Retry/InMemoryDeliveryQueue.cs
@@ -7,7 +7,8 @@ namespace AzureEventGridSimulator.Domain.Services.Retry;
/// Thread-safe in-memory implementation of the delivery queue.
/// Events are lost on restart (acceptable for a simulator).
///
-public class InMemoryDeliveryQueue(ILogger logger) : IDeliveryQueue
+public class InMemoryDeliveryQueue(TimeProvider timeProvider, ILogger logger)
+ : IDeliveryQueue
{
private readonly ConcurrentDictionary _queue = new();
@@ -55,7 +56,7 @@ public void RequeueForRetry(PendingDelivery delivery)
///
public IEnumerable GetDueDeliveries()
{
- var now = DateTime.UtcNow;
+ var now = timeProvider.GetUtcNow();
return _queue
.Values.Where(d => d.NextAttemptTime <= now)
diff --git a/src/AzureEventGridSimulator/Domain/Services/Retry/RetryDeliveryBackgroundService.cs b/src/AzureEventGridSimulator/Domain/Services/Retry/RetryDeliveryBackgroundService.cs
index b054f11..eae4438 100644
--- a/src/AzureEventGridSimulator/Domain/Services/Retry/RetryDeliveryBackgroundService.cs
+++ b/src/AzureEventGridSimulator/Domain/Services/Retry/RetryDeliveryBackgroundService.cs
@@ -14,6 +14,8 @@ public class RetryDeliveryBackgroundService(
IServiceProvider serviceProvider,
DeadLetterService deadLetterService,
IEventHistoryService eventHistoryService,
+ RetryScheduler retryScheduler,
+ TimeProvider timeProvider,
ILogger logger
) : BackgroundService
{
@@ -84,7 +86,7 @@ CancellationToken cancellationToken
)
{
// Check if event has expired (TTL)
- if (delivery.IsExpired)
+ if (delivery.IsExpired(timeProvider.GetUtcNow()))
{
logger.LogWarning(
"Event {EventId} expired for subscriber '{SubscriberName}'. TTL exceeded",
@@ -97,7 +99,7 @@ CancellationToken cancellationToken
delivery.Event.Id,
delivery.Subscriber.Name,
DeliveryStatus.DeadLettered,
- DateTimeOffset.UtcNow
+ timeProvider.GetUtcNow()
);
return;
}
@@ -117,7 +119,7 @@ CancellationToken cancellationToken
delivery.Event.Id,
delivery.Subscriber.Name,
DeliveryStatus.DeadLettered,
- DateTimeOffset.UtcNow
+ timeProvider.GetUtcNow()
);
return;
}
@@ -129,7 +131,7 @@ CancellationToken cancellationToken
delivery.AttemptCount++;
var attempt = new DeliveryAttempt
{
- AttemptTime = DateTime.UtcNow,
+ AttemptTime = timeProvider.GetUtcNow(),
AttemptNumber = delivery.AttemptCount,
Outcome = result.Outcome,
HttpStatusCode = result.HttpStatusCode,
@@ -156,7 +158,7 @@ CancellationToken cancellationToken
delivery.Event.Id,
delivery.Subscriber.Name,
DeliveryStatus.Delivered,
- DateTimeOffset.UtcNow
+ timeProvider.GetUtcNow()
);
return;
}
@@ -315,10 +317,10 @@ CancellationToken cancellationToken
// Check for immediate dead-letter conditions (HTTP 400, 401, 403, 413)
if (
result.HttpStatusCode.HasValue
- && RetryScheduler.ShouldImmediatelyDeadLetter(result.HttpStatusCode.Value)
+ && retryScheduler.ShouldImmediatelyDeadLetter(result.HttpStatusCode.Value)
)
{
- var reason = RetryScheduler.GetDeadLetterReasonForStatusCode(
+ var reason = retryScheduler.GetDeadLetterReasonForStatusCode(
result.HttpStatusCode.Value
);
@@ -334,7 +336,7 @@ CancellationToken cancellationToken
delivery.Event.Id,
delivery.Subscriber.Name,
DeliveryStatus.DeadLettered,
- DateTimeOffset.UtcNow
+ timeProvider.GetUtcNow()
);
return;
}
@@ -353,7 +355,7 @@ CancellationToken cancellationToken
delivery.Event.Id,
delivery.Subscriber.Name,
DeliveryStatus.DeadLettered,
- DateTimeOffset.UtcNow
+ timeProvider.GetUtcNow()
);
return;
}
@@ -372,13 +374,13 @@ CancellationToken cancellationToken
delivery.Event.Id,
delivery.Subscriber.Name,
DeliveryStatus.DeadLettered,
- DateTimeOffset.UtcNow
+ timeProvider.GetUtcNow()
);
return;
}
// Check if TTL will be exceeded before next retry
- if (delivery.IsExpired)
+ if (delivery.IsExpired(timeProvider.GetUtcNow()))
{
logger.LogWarning(
"Event {EventId} TTL expired during retry for '{SubscriberName}'",
@@ -391,13 +393,13 @@ CancellationToken cancellationToken
delivery.Event.Id,
delivery.Subscriber.Name,
DeliveryStatus.DeadLettered,
- DateTimeOffset.UtcNow
+ timeProvider.GetUtcNow()
);
return;
}
// Schedule retry
- delivery.NextAttemptTime = RetryScheduler.GetNextRetryTime(
+ delivery.NextAttemptTime = retryScheduler.GetNextRetryTime(
delivery.AttemptCount,
result.HttpStatusCode
);
diff --git a/src/AzureEventGridSimulator/Domain/Services/Retry/RetryScheduler.cs b/src/AzureEventGridSimulator/Domain/Services/Retry/RetryScheduler.cs
index e93f56e..5fb8dbf 100644
--- a/src/AzureEventGridSimulator/Domain/Services/Retry/RetryScheduler.cs
+++ b/src/AzureEventGridSimulator/Domain/Services/Retry/RetryScheduler.cs
@@ -3,7 +3,7 @@ namespace AzureEventGridSimulator.Domain.Services.Retry;
///
/// Calculates next retry time based on Azure Event Grid retry schedule.
///
-public static class RetryScheduler
+public class RetryScheduler
{
///
/// Azure Event Grid retry schedule with exponential backoff.
@@ -32,6 +32,13 @@ public static class RetryScheduler
///
private static readonly int[] ImmediateDeadLetterStatusCodes = [400, 401, 403, 413];
+ private readonly TimeProvider _timeProvider;
+
+ public RetryScheduler(TimeProvider timeProvider)
+ {
+ _timeProvider = timeProvider;
+ }
+
///
/// Calculates the next retry time based on attempt number and HTTP status code.
///
@@ -44,10 +51,10 @@ public static class RetryScheduler
///
/// The next retry time.
///
- public static DateTime GetNextRetryTime(int attemptNumber, int? httpStatusCode = null)
+ public DateTimeOffset GetNextRetryTime(int attemptNumber, int? httpStatusCode = null)
{
var delay = GetRetryDelay(attemptNumber, httpStatusCode);
- return DateTime.UtcNow.Add(delay);
+ return _timeProvider.GetUtcNow().Add(delay);
}
///
@@ -101,7 +108,7 @@ private static TimeSpan GetStandardDelay(int attemptNumber)
///
/// True if the status code indicates success.
///
- public static bool IsSuccessStatusCode(int statusCode)
+ public bool IsSuccessStatusCode(int statusCode)
{
return SuccessStatusCodes.Contains(statusCode);
}
@@ -115,7 +122,7 @@ public static bool IsSuccessStatusCode(int statusCode)
///
/// True if the event should be immediately dead-lettered.
///
- public static bool ShouldImmediatelyDeadLetter(int statusCode)
+ public bool ShouldImmediatelyDeadLetter(int statusCode)
{
return ImmediateDeadLetterStatusCodes.Contains(statusCode);
}
@@ -129,7 +136,7 @@ public static bool ShouldImmediatelyDeadLetter(int statusCode)
///
/// The dead-letter reason string.
///
- public static string GetDeadLetterReasonForStatusCode(int statusCode)
+ public string GetDeadLetterReasonForStatusCode(int statusCode)
{
return statusCode switch
{
diff --git a/src/AzureEventGridSimulator/Infrastructure/Middleware/SasKeyValidator.cs b/src/AzureEventGridSimulator/Infrastructure/Middleware/SasKeyValidator.cs
index 128ddd2..dbbccc8 100644
--- a/src/AzureEventGridSimulator/Infrastructure/Middleware/SasKeyValidator.cs
+++ b/src/AzureEventGridSimulator/Infrastructure/Middleware/SasKeyValidator.cs
@@ -1,4 +1,5 @@
-using System.Security.Cryptography;
+using System.Globalization;
+using System.Security.Cryptography;
using System.Text;
using System.Web;
using AzureEventGridSimulator.Domain;
@@ -6,7 +7,7 @@
namespace AzureEventGridSimulator.Infrastructure.Middleware;
-public class SasKeyValidator(ILogger logger)
+public class SasKeyValidator(TimeProvider timeProvider, ILogger logger)
{
public bool IsValid(IHeaderDictionary requestHeaders, string topicKey)
{
@@ -77,8 +78,12 @@ private bool TokenIsValid(string token, string key)
var encodedSignature = query["s"];
if (
- !DateTime.TryParse(decodedExpiration, out var tokenExpiryDateTime)
- || tokenExpiryDateTime.ToUniversalTime() <= DateTime.UtcNow
+ !DateTimeOffset.TryParse(
+ decodedExpiration,
+ CultureInfo.InvariantCulture,
+ out var tokenExpiryDateTime
+ )
+ || tokenExpiryDateTime.ToUniversalTime() <= timeProvider.GetUtcNow()
)
{
return false;
diff --git a/src/AzureEventGridSimulator/Infrastructure/Settings/Subscribers/HttpSubscriberSettings.cs b/src/AzureEventGridSimulator/Infrastructure/Settings/Subscribers/HttpSubscriberSettings.cs
index 4f61fb0..9231f5a 100644
--- a/src/AzureEventGridSimulator/Infrastructure/Settings/Subscribers/HttpSubscriberSettings.cs
+++ b/src/AzureEventGridSimulator/Infrastructure/Settings/Subscribers/HttpSubscriberSettings.cs
@@ -9,7 +9,7 @@ namespace AzureEventGridSimulator.Infrastructure.Settings.Subscribers;
///
public class HttpSubscriberSettings : ISubscriberSettings
{
- private readonly DateTime _expired = DateTime.UtcNow.AddMinutes(5);
+ private readonly DateTimeOffset _createdAt = DateTimeOffset.UtcNow;
[JsonPropertyName("endpoint")]
public string Endpoint { get; set; }
@@ -23,9 +23,6 @@ public class HttpSubscriberSettings : ISubscriberSettings
[JsonIgnore]
public Guid ValidationCode => GetValidationCode();
- [JsonIgnore]
- public bool ValidationPeriodExpired => DateTime.UtcNow > _expired;
-
[JsonPropertyName("name")]
public string Name { get; set; }
@@ -91,6 +88,20 @@ public void Validate()
DeadLetter?.Validate();
}
+ ///
+ /// Determines if the validation period has expired.
+ ///
+ ///
+ /// The current UTC time.
+ ///
+ ///
+ /// True if the 5-minute validation window has expired.
+ ///
+ public bool ValidationPeriodExpired(DateTimeOffset now)
+ {
+ return now > _createdAt.AddMinutes(5);
+ }
+
public Guid GetValidationCode()
{
return new Guid(
diff --git a/src/AzureEventGridSimulator/Infrastructure/Settings/SubscriptionSettings.cs b/src/AzureEventGridSimulator/Infrastructure/Settings/SubscriptionSettings.cs
index 7df2cc7..b897d85 100644
--- a/src/AzureEventGridSimulator/Infrastructure/Settings/SubscriptionSettings.cs
+++ b/src/AzureEventGridSimulator/Infrastructure/Settings/SubscriptionSettings.cs
@@ -6,7 +6,7 @@ namespace AzureEventGridSimulator.Infrastructure.Settings;
public class SubscriptionSettings
{
- private readonly DateTime _expired = DateTime.UtcNow.AddMinutes(5);
+ private readonly DateTimeOffset _createdAt = DateTimeOffset.UtcNow;
[JsonPropertyName("name")]
public string Name { get; set; }
@@ -37,8 +37,19 @@ public class SubscriptionSettings
[JsonIgnore]
public Guid ValidationCode => GetValidationCode();
- [JsonIgnore]
- public bool ValidationPeriodExpired => DateTime.UtcNow > _expired;
+ ///
+ /// Determines if the validation period has expired.
+ ///
+ ///
+ /// The current UTC time.
+ ///
+ ///
+ /// True if the 5-minute validation window has expired.
+ ///
+ public bool ValidationPeriodExpired(DateTimeOffset now)
+ {
+ return now > _createdAt.AddMinutes(5);
+ }
public Guid GetValidationCode()
{
diff --git a/src/AzureEventGridSimulator/Program.cs b/src/AzureEventGridSimulator/Program.cs
index 6257b67..3e48d80 100644
--- a/src/AzureEventGridSimulator/Program.cs
+++ b/src/AzureEventGridSimulator/Program.cs
@@ -279,6 +279,9 @@ IConfiguration configuration
builder.Services.AddSimulatorSettings(configuration);
builder.Services.AddMediator(Assembly.GetExecutingAssembly());
+ // Register TimeProvider for testability
+ builder.Services.AddSingleton(TimeProvider.System);
+
// Register event schema services (stateless, can be singletons)
builder.Services.AddSingleton();
builder.Services.AddSingleton();
@@ -296,6 +299,7 @@ IConfiguration configuration
builder.Services.AddSingleton();
// Register retry and dead-letter services
+ builder.Services.AddSingleton();
builder.Services.AddSingleton();
builder.Services.AddSingleton();
builder.Services.AddHostedService();