fix(notify): normalize legacy channel config and restore health diagnostics endpoint
- 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>
This commit is contained in:
@@ -18,6 +18,8 @@ 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;
|
||||
|
||||
@@ -223,6 +225,84 @@ public class NotifyWebServiceContractTests : IClassFixture<WebApplicationFactory
|
||||
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()
|
||||
{
|
||||
@@ -434,6 +514,13 @@ public class NotifyWebServiceContractTests : IClassFixture<WebApplicationFactory
|
||||
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(
|
||||
@@ -518,5 +605,3 @@ public class NotifyWebServiceContractTests : IClassFixture<WebApplicationFactory
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user