Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ trim_trailing_whitespace = true
insert_final_newline = true

[*.cs]
# Require braces on all control flow blocks
csharp_prefer_braces = true : error

# Meziantou.Analyzer rules - downgrade to suggestions for existing code
# These can be gradually fixed over time

Expand Down
2 changes: 2 additions & 0 deletions src/AzureEventGridSimulator.ServiceDefaults/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,9 @@ private static TBuilder AddOpenTelemetryExporters<TBuilder>(this TBuilder builde
);

if (useOtlpExporter)
{
builder.Services.AddOpenTelemetry().UseOtlpExporter();
}

// Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package)
//if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"]))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ private static async Task WaitForSimulatorToBeReady()
var stopwatch = Stopwatch.StartNew();

while (stopwatch.ElapsedMilliseconds < MaxStartupWaitTimeMs)
{
try
{
// Try to connect to the simulator's endpoint
Expand All @@ -87,6 +88,7 @@ private static async Task WaitForSimulatorToBeReady()
// Timeout, wait and retry
await Task.Delay(PollingIntervalMs);
}
}

throw new InvalidOperationException(
$"Simulator did not start within {MaxStartupWaitTimeMs}ms"
Expand All @@ -96,7 +98,9 @@ private static async Task WaitForSimulatorToBeReady()
private void KillExistingSimulators()
{
if (_simulatorExePath == null)
{
return;
}

try
{
Expand All @@ -115,7 +119,9 @@ private void KillExistingSimulators()
.ToArray();

foreach (var process in simulatorProcesses)
{
process.Kill();
}
}
catch
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ public void Add_ExceedsMaxCapacity_EvictsOldestEvents()
{
// Add more than max capacity
for (var i = 0; i < EventHistoryStore.MaxCapacityPerTopic + 10; i++)
{
_store.Add(CreateTestRecord($"event-{i}"));
}

_store.Count.ShouldBe(EventHistoryStore.MaxCapacityPerTopic);
_store.TotalEventsReceived.ShouldBe(EventHistoryStore.MaxCapacityPerTopic + 10);
Expand All @@ -54,7 +56,9 @@ public void Add_ExceedsMaxCapacity_EvictsOldestEvents()
public void Add_ExceedsMaxCapacity_OldestEventsAreRemoved()
{
for (var i = 0; i < EventHistoryStore.MaxCapacityPerTopic + 5; i++)
{
_store.Add(CreateTestRecord($"event-{i}"));
}

// First 5 events should be evicted
_store.Get("event-0").ShouldBeNull();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -407,16 +407,24 @@ private static DefaultHttpContext CreateBinaryModeContext(
};

if (time != null)
{
context.Request.Headers[Constants.CeTimeHeader] = time;
}

if (subject != null)
{
context.Request.Headers[Constants.CeSubjectHeader] = subject;
}

if (dataContentType != null)
{
context.Request.Headers[Constants.CeDataContentTypeHeader] = dataContentType;
}

if (dataSchema != null)
{
context.Request.Headers[Constants.CeDataSchemaHeader] = dataSchema;
}

return context;
}
Expand Down Expand Up @@ -463,7 +471,9 @@ private static DefaultHttpContext CreateBinaryModeContextWithRawHeaders(
};

if (subject != null)
{
context.Request.Headers[Constants.CeSubjectHeader] = subject;
}

return context;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,16 +66,24 @@ public static HttpContext CreateCloudEventsBinaryModeContext(
};

if (time != null)
{
context.Request.Headers[Constants.CeTimeHeader] = time;
}

if (subject != null)
{
context.Request.Headers[Constants.CeSubjectHeader] = subject;
}

if (dataContentType != null)
{
context.Request.Headers[Constants.CeDataContentTypeHeader] = dataContentType;
}

if (dataSchema != null)
{
context.Request.Headers[Constants.CeDataSchemaHeader] = dataSchema;
}

return context;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@ public void TestFilterSettingsValidationWithValidNumberOfAdvancedFilterSettings(
AdvancedFilters = new List<AdvancedFilterSetting>(),
};
for (byte i = 0; i < n; i++)
{
filterConfig.AdvancedFilters.Add(GetValidAdvancedFilter());
}

GetValidSimulatorSettings(filterConfig).Validate();
});
Expand All @@ -75,7 +77,9 @@ public void TestFilterSettingsValidationWithTooManyAdvancedFilters()
};
// Azure Event Grid allows up to 25 filters, so 26 should fail
for (var i = 0; i < 26; i++)
{
filterConfig.AdvancedFilters.Add(GetValidAdvancedFilter());
}

var exception = Should.Throw<ArgumentException>(() =>
GetValidSimulatorSettings(filterConfig).Validate()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ public void Dispose()
{
// Clean up temp folder
if (Directory.Exists(_tempFolder))
{
Directory.Delete(_tempFolder, true);
}
}

private static PendingDelivery CreatePendingDelivery(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ public HttpEventDeliveryServiceTests()
public void Dispose()
{
foreach (var client in _httpClients)
{
client.Dispose();
}

_httpClients.Clear();
}
Expand Down Expand Up @@ -158,7 +160,9 @@ public async Task GivenMultipleAttempts_WhenDelivering_ThenIncludesDeliveryCount
var httpClientFactory = CreateMockHttpClientFactory(captureHeaders: headers =>
{
if (headers.TryGetValues(Constants.AegDeliveryCountHeader, out var values))
{
capturedDeliveryCount = values.FirstOrDefault();
}
});
var service = new HttpEventDeliveryService(httpClientFactory, _formatterFactory, _logger);
var delivery = CreatePendingDelivery();
Expand Down Expand Up @@ -298,7 +302,9 @@ CancellationToken cancellationToken
_responseAction?.Invoke();

if (_exception != null)
{
throw _exception;
}

var response = new HttpResponseMessage(_statusCode)
{
Expand All @@ -315,7 +321,9 @@ protected override void Dispose(bool disposing)
if (disposing)
{
foreach (var response in _responses)
{
response.Dispose();
}

_responses.Clear();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@ private static PendingDelivery CreatePendingDelivery(
RetryPolicySettings? retryPolicy = null;

if (retryEnabled.HasValue || ttlMinutes.HasValue || maxAttempts.HasValue)
{
retryPolicy = new RetryPolicySettings
{
Enabled = retryEnabled ?? true,
EventTimeToLiveInMinutes = ttlMinutes ?? 1440,
MaxDeliveryAttempts = maxAttempts ?? 30,
};
}

var subscriber = new HttpSubscriberSettings
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,18 @@ public async Task<IActionResult> Post()

// Events are parsed and validated by EventParsingMiddleware
if (HttpContext.Items["ParsedEvents"] is not SimulatorEvent[] events)
{
throw new InvalidOperationException(
"ParsedEvents not found in HttpContext. Ensure EventParsingMiddleware is configured."
);
}

if (HttpContext.Items["DetectedSchema"] is not EventSchema detectedSchema)
{
throw new InvalidOperationException(
"DetectedSchema not found in HttpContext. Ensure EventParsingMiddleware is configured."
);
}

await mediator.Send(
new SendNotificationEventsToSubscriberCommand(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public async Task<IActionResult> Get(Guid id)
);

if (!isValid)
{
return BadRequest(
new ErrorMessage(
HttpStatusCode.BadRequest,
Expand All @@ -36,6 +37,7 @@ public async Task<IActionResult> Get(Guid id)
ErrorDetailCodes.InputJsonInvalid
)
);
}

return Ok("Webhook successfully validated as a subscription endpoint");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ CancellationToken cancellationToken

// Record events for dashboard
foreach (var evt in request.Events)
{
eventHistoryService.RecordEventReceived(evt, request.Topic, request.InputSchema);
}

// Enrich events with topic information
EnrichEvents(request.Events, request.Topic.Name);
Expand Down Expand Up @@ -75,11 +77,13 @@ CancellationToken cancellationToken
.ToArray();

foreach (var filteredEvent in eventsFilteredOutByAllSubscribers)
{
logger.LogWarning(
"All subscribers of topic '{TopicName}' filtered out event {EventId}",
request.Topic.Name,
filteredEvent.Id
);
}

// Enqueue events for each subscriber
var enqueuedCount = 0;
Expand Down Expand Up @@ -142,11 +146,13 @@ subscriber is HttpSubscriberSettings httpSubscriber
}

if (enqueuedCount > 0)
{
logger.LogDebug(
"Enqueued {Count} event delivery(ies) for topic '{TopicName}'",
enqueuedCount,
request.Topic.Name
);
}

return Task.CompletedTask;
}
Expand All @@ -157,6 +163,7 @@ private static void EnrichEvents(SimulatorEvent[] events, string topicName)
$"/subscriptions/{Guid.Empty:D}/resourceGroups/eventGridSimulator/providers/Microsoft.EventGrid/topics/{topicName}";

foreach (var evt in events)
{
if (evt.Schema == EventSchema.EventGridSchema && evt.EventGridEvent != null)
{
evt.EventGridEvent.SetTopic(topicPath);
Expand All @@ -167,7 +174,10 @@ private static void EnrichEvents(SimulatorEvent[] events, string topicName)
// CloudEvents use 'source' which is already set
// Optionally set it to the topic path if not already set
if (string.IsNullOrEmpty(evt.CloudEvent.Source))
{
evt.CloudEvent.Source = topicPath;
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,17 @@ CancellationToken cancellationToken
)
{
foreach (var enabledTopic in simulatorSettings.Topics.Where(o => !o.Disabled))
// Only HTTP subscribers need validation (Service Bus subscribers don't use webhook validation)
foreach (
var subscriber in enabledTopic.Subscribers.HttpSubscribers.Where(o =>
!o.DisableValidation && !o.Disabled
{
// Only HTTP subscribers need validation (Service Bus subscribers don't use webhook validation)
foreach (
var subscriber in enabledTopic.Subscribers.HttpSubscribers.Where(o =>
!o.DisableValidation && !o.Disabled
)
)
)
await ValidateSubscription(enabledTopic, subscriber);
{
await ValidateSubscription(enabledTopic, subscriber);
}
}
}

private async Task ValidateSubscription(
Expand Down
10 changes: 10 additions & 0 deletions src/AzureEventGridSimulator/Domain/Entities/CloudEvent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,30 +122,40 @@ public void Validate()

// Validate required fields are non-empty
if (string.IsNullOrWhiteSpace(Id))
{
throw new InvalidOperationException(
$"This resource is configured for '{SchemaName}' schema and requires 'id' property to be set."
);
}

if (string.IsNullOrWhiteSpace(Type))
{
throw new InvalidOperationException(
$"This resource is configured for '{SchemaName}' schema and requires 'eventType' property to be set."
);
}

// Azure does NOT enforce field length limits for type or subject

// Optional: time - if present, must be valid date/time
// Note: Azure is lenient and accepts time without timezone (though CloudEvents spec recommends RFC 3339 with timezone)
if (!string.IsNullOrEmpty(Time) && !TimeIsValid)
{
throw new InvalidOperationException(
"The event time property 'time' was not a valid date/time."
);
}

// Optional: dataschema - if present, must be a valid URI
if (!string.IsNullOrEmpty(DataSchema))
{
if (!Uri.TryCreate(DataSchema, UriKind.RelativeOrAbsolute, out _))
{
throw new InvalidOperationException(
$"This resource is configured for '{SchemaName}' schema and requires 'dataschema' property to be a valid URI."
);
}
}

// Note: Azure Event Grid is lenient and accepts both data and data_base64
// The spec says they are mutually exclusive, but Azure doesn't enforce this
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,9 @@ public void AddOrUpdateDelivery(DeliveryRecord delivery)
d.SubscriberName == delivery.SubscriberName
);
if (existing != null)
{
Deliveries.Remove(existing);
}

Deliveries.Add(delivery);
}
Expand Down
Loading
Loading