- Add legacy channel config normalization for unmapped smtpHost, webhookUrl,
channel fields into canonical NotifyChannelConfig
- Restore GET /channels/{channelId}/health endpoint
- Add JsonConverter attribute to ChannelHealthStatus enum
- Add test coverage for legacy row shapes and health contract
- Remove hosted services from test override to isolate channel tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
608 lines
21 KiB
C#
608 lines
21 KiB
C#
// -----------------------------------------------------------------------------
|
|
// StellaOps.Notify.WebService.Tests / W1 / NotifyWebServiceContractTests.cs
|
|
// W1 contract tests for Notify.WebService endpoints (send notification, query status) — OpenAPI snapshot.
|
|
// Task: NOTIFY-5100-012
|
|
// -----------------------------------------------------------------------------
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Net;
|
|
using System.Net.Http;
|
|
using System.Net.Http.Headers;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Nodes;
|
|
using System.Threading.Tasks;
|
|
using FluentAssertions;
|
|
using Microsoft.AspNetCore.Mvc.Testing;
|
|
using Microsoft.AspNetCore.TestHost;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using StellaOps.Notify.Persistence.Postgres.Models;
|
|
using StellaOps.Notify.Persistence.Postgres.Repositories;
|
|
using Xunit;
|
|
using Xunit.v3;
|
|
|
|
namespace StellaOps.Notify.WebService.Tests.W1;
|
|
|
|
/// <summary>
|
|
/// W1 contract tests for Notify WebService endpoints.
|
|
/// Tests verify endpoint contracts (request/response shapes), status codes,
|
|
/// and OpenAPI compliance.
|
|
/// </summary>
|
|
public class NotifyWebServiceContractTests : IClassFixture<WebApplicationFactory<Program>>, IAsyncLifetime
|
|
{
|
|
private const string SigningKey = "super-secret-test-key-for-contract-tests-1234567890";
|
|
private const string Issuer = "test-issuer";
|
|
private const string Audience = "notify";
|
|
private const string TestTenantId = "tenant-contract-test";
|
|
|
|
private readonly WebApplicationFactory<Program> _factory;
|
|
private readonly string _operatorToken;
|
|
private readonly string _viewerToken;
|
|
private readonly string _adminToken;
|
|
|
|
public NotifyWebServiceContractTests(WebApplicationFactory<Program> factory)
|
|
{
|
|
_factory = factory.WithWebHostBuilder(builder =>
|
|
{
|
|
builder.ConfigureAppConfiguration((_, config) =>
|
|
{
|
|
config.AddInMemoryCollection(new Dictionary<string, string?>
|
|
{
|
|
["notify:storage:driver"] = "memory",
|
|
["notify:authority:enabled"] = "false",
|
|
["notify:authority:developmentSigningKey"] = SigningKey,
|
|
["notify:authority:issuer"] = Issuer,
|
|
["notify:authority:audiences:0"] = Audience,
|
|
["notify:authority:allowAnonymousFallback"] = "false",
|
|
["notify:authority:adminScope"] = "notify.admin",
|
|
["notify:authority:operatorScope"] = "notify.operator",
|
|
["notify:authority:viewerScope"] = "notify.viewer",
|
|
["notify:telemetry:enableRequestLogging"] = "false",
|
|
});
|
|
});
|
|
builder.ConfigureTestServices(services =>
|
|
{
|
|
NotifyTestServiceOverrides.ReplaceWithInMemory(services, signingKey: SigningKey, issuer: Issuer, audience: Audience);
|
|
});
|
|
});
|
|
|
|
_viewerToken = CreateToken("notify.viewer");
|
|
_operatorToken = CreateToken("notify.viewer", "notify.operator");
|
|
_adminToken = CreateToken("notify.viewer", "notify.operator", "notify.admin");
|
|
}
|
|
|
|
public ValueTask InitializeAsync() => ValueTask.CompletedTask;
|
|
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
|
|
|
#region Health Endpoints Contract
|
|
|
|
[Fact]
|
|
public async Task HealthEndpoint_ReturnsOkWithStatus()
|
|
{
|
|
// Arrange
|
|
var client = _factory.CreateClient();
|
|
|
|
// Act
|
|
var response = await client.GetAsync("/healthz", CancellationToken.None);
|
|
|
|
// Assert
|
|
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
var content = await response.Content.ReadAsStringAsync(CancellationToken.None);
|
|
var json = JsonNode.Parse(content);
|
|
json?["status"]?.GetValue<string>().Should().Be("ok");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReadyEndpoint_ReturnsHealthStatus()
|
|
{
|
|
// Arrange
|
|
var client = _factory.CreateClient();
|
|
|
|
// Act
|
|
var response = await client.GetAsync("/readyz", CancellationToken.None);
|
|
|
|
// Assert
|
|
response.StatusCode.Should().BeOneOf(HttpStatusCode.OK, HttpStatusCode.ServiceUnavailable);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Rules Endpoints Contract
|
|
|
|
[Fact]
|
|
public async Task ListRules_ReturnsJsonArray()
|
|
{
|
|
// Arrange
|
|
var client = CreateAuthenticatedClient(_viewerToken);
|
|
|
|
// Act
|
|
var response = await client.GetAsync("/api/v1/notify/rules", CancellationToken.None);
|
|
|
|
// Assert
|
|
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
response.Content.Headers.ContentType?.MediaType.Should().Be("application/json");
|
|
|
|
var content = await response.Content.ReadAsStringAsync(CancellationToken.None);
|
|
var json = JsonNode.Parse(content);
|
|
json.Should().NotBeNull();
|
|
json!.AsArray().Should().NotBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateRule_ValidPayload_Returns201WithLocation()
|
|
{
|
|
// Arrange
|
|
var client = CreateAuthenticatedClient(_operatorToken);
|
|
|
|
var ruleId = Guid.NewGuid().ToString();
|
|
var payload = CreateRulePayload(ruleId);
|
|
|
|
// Act
|
|
var response = await client.PostAsync(
|
|
"/api/v1/notify/rules",
|
|
new StringContent(payload.ToJsonString(), Encoding.UTF8, "application/json"),
|
|
CancellationToken.None);
|
|
|
|
// Assert
|
|
response.StatusCode.Should().Be(HttpStatusCode.Created);
|
|
response.Headers.Location.Should().NotBeNull();
|
|
response.Headers.Location!.ToString().Should().Contain(ruleId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateRule_InvalidPayload_Returns400()
|
|
{
|
|
// Arrange
|
|
var client = CreateAuthenticatedClient(_operatorToken);
|
|
|
|
var invalidPayload = new JsonObject { ["invalid"] = "data" };
|
|
|
|
// Act
|
|
var response = await client.PostAsync(
|
|
"/api/v1/notify/rules",
|
|
new StringContent(invalidPayload.ToJsonString(), Encoding.UTF8, "application/json"));
|
|
|
|
// Assert
|
|
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetRule_NotFound_Returns404()
|
|
{
|
|
// Arrange
|
|
var client = CreateAuthenticatedClient(_viewerToken);
|
|
|
|
// Act
|
|
var response = await client.GetAsync($"/api/v1/notify/rules/{Guid.NewGuid()}", CancellationToken.None);
|
|
|
|
// Assert
|
|
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DeleteRule_Existing_Returns204()
|
|
{
|
|
// Arrange
|
|
var client = CreateAuthenticatedClient(_operatorToken);
|
|
|
|
// First create a rule
|
|
var ruleId = Guid.NewGuid().ToString();
|
|
var payload = CreateRulePayload(ruleId);
|
|
await client.PostAsync(
|
|
"/api/v1/notify/rules",
|
|
new StringContent(payload.ToJsonString(), Encoding.UTF8, "application/json"),
|
|
CancellationToken.None);
|
|
|
|
// Act
|
|
var response = await client.DeleteAsync($"/api/v1/notify/rules/{ruleId}", CancellationToken.None);
|
|
|
|
// Assert
|
|
response.StatusCode.Should().Be(HttpStatusCode.NoContent);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Channels Endpoints Contract
|
|
|
|
[Fact]
|
|
public async Task ListChannels_ReturnsJsonArray()
|
|
{
|
|
// Arrange
|
|
var client = CreateAuthenticatedClient(_viewerToken);
|
|
|
|
// Act
|
|
var response = await client.GetAsync("/api/v1/notify/channels", CancellationToken.None);
|
|
|
|
// Assert
|
|
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
response.Content.Headers.ContentType?.MediaType.Should().Be("application/json");
|
|
|
|
var content = await response.Content.ReadAsStringAsync(CancellationToken.None);
|
|
var json = JsonNode.Parse(content);
|
|
json.Should().NotBeNull();
|
|
json!.AsArray().Should().NotBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ListChannels_LegacyConfigWithoutSecretRef_ReturnsNormalizedChannel()
|
|
{
|
|
// Arrange
|
|
await SeedChannelAsync(new ChannelEntity
|
|
{
|
|
Id = Guid.Parse("e0000001-0000-0000-0000-0000000000aa"),
|
|
TenantId = TestTenantId,
|
|
Name = "legacy-slack",
|
|
ChannelType = ChannelType.Slack,
|
|
Enabled = true,
|
|
Config = """
|
|
{
|
|
"channel": "#security-alerts",
|
|
"username": "StellaOps",
|
|
"webhookUrl": "https://hooks.slack.example.com/services/demo"
|
|
}
|
|
""",
|
|
Metadata = "{}",
|
|
CreatedAt = DateTimeOffset.UtcNow,
|
|
UpdatedAt = DateTimeOffset.UtcNow
|
|
});
|
|
|
|
var client = CreateAuthenticatedClient(_viewerToken);
|
|
|
|
// Act
|
|
var response = await client.GetAsync("/api/v1/notify/channels", CancellationToken.None);
|
|
|
|
// Assert
|
|
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
var content = await response.Content.ReadAsStringAsync(CancellationToken.None);
|
|
var json = JsonNode.Parse(content)?.AsArray();
|
|
var legacyChannel = json?
|
|
.Select(node => node?.AsObject())
|
|
.FirstOrDefault(node => node?["name"]?.GetValue<string>() == "legacy-slack");
|
|
|
|
legacyChannel.Should().NotBeNull();
|
|
legacyChannel!["config"]?["secretRef"]?.GetValue<string>().Should().Be("legacy://notify/channels/e00000010000000000000000000000aa");
|
|
legacyChannel["config"]?["target"]?.GetValue<string>().Should().Be("#security-alerts");
|
|
legacyChannel["config"]?["endpoint"]?.GetValue<string>().Should().Be("https://hooks.slack.example.com/services/demo");
|
|
legacyChannel["config"]?["properties"]?["username"]?.GetValue<string>().Should().Be("StellaOps");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetChannelHealth_ExistingChannel_ReturnsDiagnostics()
|
|
{
|
|
var channelId = Guid.Parse("e0000001-0000-0000-0000-0000000000ab");
|
|
await SeedChannelAsync(new ChannelEntity
|
|
{
|
|
Id = channelId,
|
|
TenantId = TestTenantId,
|
|
Name = "health-webhook",
|
|
ChannelType = ChannelType.Webhook,
|
|
Enabled = true,
|
|
Config = """
|
|
{
|
|
"secretRef": "secret://notify/health-webhook",
|
|
"endpoint": "https://notify.example.test/hooks/demo"
|
|
}
|
|
""",
|
|
Metadata = "{}",
|
|
CreatedAt = DateTimeOffset.UtcNow,
|
|
UpdatedAt = DateTimeOffset.UtcNow
|
|
});
|
|
|
|
var client = CreateAuthenticatedClient(_viewerToken);
|
|
|
|
var response = await client.GetAsync($"/api/v1/notify/channels/{channelId}/health", CancellationToken.None);
|
|
|
|
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
var content = await response.Content.ReadAsStringAsync(CancellationToken.None);
|
|
var json = JsonNode.Parse(content)?.AsObject();
|
|
json.Should().NotBeNull();
|
|
json!["channelId"]?.GetValue<string>().Should().Be(channelId.ToString());
|
|
json["status"]?.GetValue<string>().Should().NotBeNullOrWhiteSpace();
|
|
json["metadata"]?["target"]?.GetValue<string>().Should().Be("https://notify.example.test/hooks/demo");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateChannel_ValidPayload_Returns201()
|
|
{
|
|
// Arrange
|
|
var client = CreateAuthenticatedClient(_operatorToken);
|
|
|
|
var channelId = Guid.NewGuid().ToString();
|
|
var payload = CreateChannelPayload(channelId);
|
|
|
|
// Act
|
|
var response = await client.PostAsync(
|
|
"/api/v1/notify/channels",
|
|
new StringContent(payload.ToJsonString(), Encoding.UTF8, "application/json"),
|
|
CancellationToken.None);
|
|
|
|
// Assert
|
|
response.StatusCode.Should().Be(HttpStatusCode.Created);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Templates Endpoints Contract
|
|
|
|
[Fact]
|
|
public async Task ListTemplates_ReturnsJsonArray()
|
|
{
|
|
// Arrange
|
|
var client = CreateAuthenticatedClient(_viewerToken);
|
|
|
|
// Act
|
|
var response = await client.GetAsync("/api/v1/notify/templates", CancellationToken.None);
|
|
|
|
// Assert
|
|
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
var content = await response.Content.ReadAsStringAsync(CancellationToken.None);
|
|
var json = JsonNode.Parse(content);
|
|
json.Should().NotBeNull();
|
|
json!.AsArray().Should().NotBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateTemplate_ValidPayload_Returns201()
|
|
{
|
|
// Arrange
|
|
var client = CreateAuthenticatedClient(_operatorToken);
|
|
|
|
var templateId = Guid.NewGuid();
|
|
var payload = CreateTemplatePayload(templateId);
|
|
|
|
// Act
|
|
var response = await client.PostAsync(
|
|
"/api/v1/notify/templates",
|
|
new StringContent(payload.ToJsonString(), Encoding.UTF8, "application/json"),
|
|
CancellationToken.None);
|
|
|
|
// Assert
|
|
response.StatusCode.Should().Be(HttpStatusCode.Created);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Deliveries Endpoints Contract
|
|
|
|
[Fact]
|
|
public async Task CreateDelivery_ValidPayload_Returns201OrAccepted()
|
|
{
|
|
// Arrange
|
|
var client = CreateAuthenticatedClient(_operatorToken);
|
|
|
|
var deliveryId = Guid.NewGuid();
|
|
var payload = CreateDeliveryPayload(deliveryId);
|
|
|
|
// Act
|
|
var response = await client.PostAsync(
|
|
"/api/v1/notify/deliveries",
|
|
new StringContent(payload.ToJsonString(), Encoding.UTF8, "application/json"),
|
|
CancellationToken.None);
|
|
|
|
// Assert - can be 201 Created or 202 Accepted depending on processing
|
|
response.StatusCode.Should().BeOneOf(HttpStatusCode.Created, HttpStatusCode.Accepted);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ListDeliveries_ReturnsJsonArrayWithPagination()
|
|
{
|
|
// Arrange
|
|
var client = CreateAuthenticatedClient(_viewerToken);
|
|
|
|
// Act
|
|
var response = await client.GetAsync("/api/v1/notify/deliveries?limit=10", CancellationToken.None);
|
|
|
|
// Assert
|
|
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
var content = await response.Content.ReadAsStringAsync(CancellationToken.None);
|
|
var json = JsonNode.Parse(content);
|
|
json.Should().NotBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetDelivery_NotFound_Returns404()
|
|
{
|
|
// Arrange
|
|
var client = CreateAuthenticatedClient(_viewerToken);
|
|
|
|
// Act
|
|
var response = await client.GetAsync($"/api/v1/notify/deliveries/{Guid.NewGuid()}", CancellationToken.None);
|
|
|
|
// Assert
|
|
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Normalize Endpoints Contract (Internal)
|
|
|
|
[Fact]
|
|
public async Task NormalizeRule_ValidPayload_ReturnsUpgradedSchema()
|
|
{
|
|
// Arrange
|
|
var client = CreateAuthenticatedClient(_adminToken);
|
|
|
|
var payload = CreateRulePayload(Guid.NewGuid().ToString());
|
|
|
|
// Act
|
|
var response = await client.PostAsync(
|
|
"/internal/notify/rules/normalize",
|
|
new StringContent(payload.ToJsonString(), Encoding.UTF8, "application/json"),
|
|
CancellationToken.None);
|
|
|
|
// Assert
|
|
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
var content = await response.Content.ReadAsStringAsync(CancellationToken.None);
|
|
var json = JsonNode.Parse(content);
|
|
json?["schemaVersion"]?.GetValue<string>().Should().NotBeNullOrEmpty();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Response Shape Validation
|
|
|
|
[Fact]
|
|
public async Task RuleResponse_ContainsRequiredFields()
|
|
{
|
|
// Arrange
|
|
var client = CreateAuthenticatedClient(_operatorToken);
|
|
|
|
var ruleId = Guid.NewGuid().ToString();
|
|
var payload = CreateRulePayload(ruleId);
|
|
await client.PostAsync(
|
|
"/api/v1/notify/rules",
|
|
new StringContent(payload.ToJsonString(), Encoding.UTF8, "application/json"),
|
|
CancellationToken.None);
|
|
|
|
// Act
|
|
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _viewerToken);
|
|
var response = await client.GetAsync($"/api/v1/notify/rules/{ruleId}", CancellationToken.None);
|
|
|
|
// Assert
|
|
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
var content = await response.Content.ReadAsStringAsync(CancellationToken.None);
|
|
var json = JsonNode.Parse(content);
|
|
|
|
// Verify required fields exist
|
|
json?["ruleId"].Should().NotBeNull();
|
|
json?["tenantId"].Should().NotBeNull();
|
|
json?["schemaVersion"].Should().NotBeNull();
|
|
json?["name"].Should().NotBeNull();
|
|
json?["enabled"].Should().NotBeNull();
|
|
json?["actions"].Should().NotBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ChannelResponse_ContainsRequiredFields()
|
|
{
|
|
// Arrange
|
|
var client = CreateAuthenticatedClient(_operatorToken);
|
|
|
|
var channelId = Guid.NewGuid().ToString();
|
|
var payload = CreateChannelPayload(channelId);
|
|
await client.PostAsync(
|
|
"/api/v1/notify/channels",
|
|
new StringContent(payload.ToJsonString(), Encoding.UTF8, "application/json"),
|
|
CancellationToken.None);
|
|
|
|
// Act
|
|
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _viewerToken);
|
|
var response = await client.GetAsync($"/api/v1/notify/channels/{channelId}", CancellationToken.None);
|
|
|
|
// Assert
|
|
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
var content = await response.Content.ReadAsStringAsync(CancellationToken.None);
|
|
var json = JsonNode.Parse(content);
|
|
|
|
json?["channelId"].Should().NotBeNull();
|
|
json?["tenantId"].Should().NotBeNull();
|
|
json?["type"].Should().NotBeNull();
|
|
json?["name"].Should().NotBeNull();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Helper Methods
|
|
|
|
private HttpClient CreateAuthenticatedClient(string token)
|
|
{
|
|
var client = _factory.CreateClient();
|
|
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
|
client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", TestTenantId);
|
|
return client;
|
|
}
|
|
|
|
private async Task SeedChannelAsync(ChannelEntity entity)
|
|
{
|
|
using var scope = _factory.Services.CreateScope();
|
|
var repository = scope.ServiceProvider.GetRequiredService<IChannelRepository>();
|
|
await repository.CreateAsync(entity, CancellationToken.None);
|
|
}
|
|
|
|
private static string CreateToken(params string[] scopes)
|
|
{
|
|
return NotifyTestServiceOverrides.CreateTestToken(
|
|
SigningKey, Issuer, Audience, scopes, tenantId: TestTenantId);
|
|
}
|
|
|
|
private static JsonObject CreateRulePayload(string ruleId)
|
|
{
|
|
return new JsonObject
|
|
{
|
|
["schemaVersion"] = "notify.rule@1",
|
|
["ruleId"] = ruleId,
|
|
["tenantId"] = TestTenantId,
|
|
["name"] = $"Test Rule {ruleId}",
|
|
["description"] = "Contract test rule",
|
|
["enabled"] = true,
|
|
["match"] = new JsonObject
|
|
{
|
|
["eventKinds"] = new JsonArray { "scan.completed" }
|
|
},
|
|
["actions"] = new JsonArray
|
|
{
|
|
new JsonObject
|
|
{
|
|
["actionId"] = Guid.NewGuid().ToString(),
|
|
["channel"] = Guid.NewGuid().ToString(),
|
|
["template"] = "default",
|
|
["enabled"] = true
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
private static JsonObject CreateChannelPayload(string channelId)
|
|
{
|
|
return new JsonObject
|
|
{
|
|
["schemaVersion"] = "notify.channel@1",
|
|
["channelId"] = channelId,
|
|
["tenantId"] = TestTenantId,
|
|
["type"] = "email",
|
|
["name"] = $"Test Channel {channelId}",
|
|
["enabled"] = true,
|
|
["config"] = new JsonObject
|
|
{
|
|
["secretRef"] = "vault://notify/channels/test",
|
|
["target"] = "test@example.com"
|
|
}
|
|
};
|
|
}
|
|
|
|
private static JsonObject CreateTemplatePayload(Guid templateId)
|
|
{
|
|
return new JsonObject
|
|
{
|
|
["schemaVersion"] = "notify.template@1",
|
|
["templateId"] = templateId.ToString(),
|
|
["tenantId"] = TestTenantId,
|
|
["channelType"] = "email",
|
|
["key"] = "scan-report",
|
|
["locale"] = "en",
|
|
["body"] = "Scan completed for {{event.payload.image}}",
|
|
["renderMode"] = "markdown"
|
|
};
|
|
}
|
|
|
|
private static JsonObject CreateDeliveryPayload(Guid deliveryId)
|
|
{
|
|
return new JsonObject
|
|
{
|
|
["deliveryId"] = deliveryId.ToString(),
|
|
["tenantId"] = TestTenantId,
|
|
["ruleId"] = Guid.NewGuid().ToString(),
|
|
["actionId"] = Guid.NewGuid().ToString(),
|
|
["eventId"] = Guid.NewGuid().ToString(),
|
|
["kind"] = "scanner.report.ready",
|
|
["status"] = "pending"
|
|
};
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
|
|
|