using System; using System.Collections.Generic; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using StellaOps.Notify.Engine; using StellaOps.Notify.Models; using Xunit; using StellaOps.TestKit; namespace StellaOps.Notify.Connectors.Slack.Tests; public sealed class SlackChannelTestProviderTests { private static readonly ChannelTestPreviewRequest EmptyRequest = new( TargetOverride: null, TemplateId: null, Title: null, Summary: null, Body: null, TextBody: null, Locale: null, Metadata: new Dictionary(), Attachments: new List()); [Trait("Category", TestCategories.Unit)] [Fact] public async Task BuildPreviewAsync_ProducesDeterministicMetadata() { var provider = new SlackChannelTestProvider(); var channel = CreateChannel(properties: new Dictionary { ["workspace"] = "stellaops-sec", ["botToken"] = "xoxb-123456789012-abcdefghijklmnop" }); var context = new ChannelTestPreviewContext( channel.TenantId, channel, channel.Config.Target!, EmptyRequest, Timestamp: new DateTimeOffset(2025, 10, 20, 12, 00, 00, TimeSpan.Zero), TraceId: "trace-001"); var result = await provider.BuildPreviewAsync(context, CancellationToken.None); Assert.Equal("slack", result.Preview.ChannelType.ToString().ToLowerInvariant()); Assert.Equal(channel.Config.Target, result.Preview.Target); Assert.Equal("chat:write,chat:write.public", result.Metadata["slack.scopes.required"]); Assert.Equal("stellaops-sec", result.Metadata["slack.config.workspace"]); var redactedToken = result.Metadata["slack.config.botToken"]; Assert.DoesNotContain("abcdefghijklmnop", redactedToken); Assert.StartsWith("xoxb-", redactedToken); Assert.EndsWith("mnop", redactedToken); using var parsed = JsonDocument.Parse(result.Preview.Body); var contextText = parsed.RootElement .GetProperty("blocks")[1] .GetProperty("elements")[0] .GetProperty("text") .GetString(); Assert.NotNull(contextText); Assert.Contains("trace-001", contextText); Assert.Equal(ComputeSecretHash(channel.Config.SecretRef), result.Metadata["slack.secretRef.hash"]); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task BuildPreviewAsync_RedactsSensitiveProperties() { var provider = new SlackChannelTestProvider(); var channel = CreateChannel(properties: new Dictionary { ["SigningSecret"] = "whsec_super-secret-value", ["apiToken"] = "xoxs-000000000000-super", ["endpoint"] = "https://hooks.slack.com/services/T000/B000/AAA" }); var context = new ChannelTestPreviewContext( channel.TenantId, channel, channel.Config.Target!, EmptyRequest, Timestamp: DateTimeOffset.UtcNow, TraceId: "trace-002"); var result = await provider.BuildPreviewAsync(context, CancellationToken.None); Assert.Equal("***", result.Metadata["slack.config.SigningSecret"]); Assert.DoesNotContain("xoxs-000000000000-super", result.Metadata["slack.config.apiToken"]); Assert.Equal("https://hooks.slack.com/services/T000/B000/AAA", result.Metadata["slack.config.endpoint"]); } private static NotifyChannel CreateChannel(IDictionary properties) { return NotifyChannel.Create( channelId: "channel-slack-sec-ops", tenantId: "tenant-sec", name: "slack:sec-ops", type: NotifyChannelType.Slack, config: NotifyChannelConfig.Create( secretRef: "ref://notify/channels/slack/sec-ops", target: "#sec-ops", properties: properties)); } private static string ComputeSecretHash(string secretRef) { using var sha = System.Security.Cryptography.SHA256.Create(); using StellaOps.TestKit; var bytes = System.Text.Encoding.UTF8.GetBytes(secretRef.Trim()); var hash = sha.ComputeHash(bytes); return System.Convert.ToHexString(hash, 0, 8).ToLowerInvariant(); } }