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; namespace StellaOps.Notify.Connectors.Teams.Tests; public sealed class TeamsChannelTestProviderTests { [Fact] public async Task BuildPreviewAsync_EmitsFallbackMetadata() { var provider = new TeamsChannelTestProvider(); var channel = CreateChannel( endpoint: "https://contoso.webhook.office.com/webhookb2/tenant@uuid/IncomingWebhook/abcdef0123456789", properties: new Dictionary { ["team"] = "secops", ["webhookKey"] = "s3cr3t-value-with-key-fragment", ["tenant"] = "contoso.onmicrosoft.com" }); var request = new ChannelTestPreviewRequest( TargetOverride: null, TemplateId: null, Title: "Notify Critical Finding", Summary: "Critical container vulnerability detected.", Body: "CVSS 9.8 vulnerability detected in ubuntu:22.04 base layer.", TextBody: null, Locale: "en-US", Metadata: new Dictionary(), Attachments: new List()); var context = new ChannelTestPreviewContext( channel.TenantId, channel, channel.Config.Endpoint!, request, new DateTimeOffset(2025, 10, 20, 10, 0, 0, TimeSpan.Zero), TraceId: "trace-teams-001"); var result = await provider.BuildPreviewAsync(context, CancellationToken.None); Assert.Equal(NotifyChannelType.Teams, result.Preview.ChannelType); Assert.Equal(channel.Config.Endpoint, result.Preview.Target); Assert.Equal("Critical container vulnerability detected.", result.Preview.Summary); Assert.NotNull(result.Metadata); Assert.Equal(channel.Config.Endpoint, result.Metadata["teams.webhook"]); Assert.Equal("1.5", result.Metadata["teams.card.version"]); var fallback = result.Metadata["teams.fallbackText"]; Assert.Equal(result.Preview.TextBody, fallback); Assert.Equal("Critical container vulnerability detected.", fallback); Assert.Equal(ComputeSecretHash(channel.Config.SecretRef), result.Metadata["teams.secretRef.hash"]); Assert.Equal("***", result.Metadata["teams.config.webhookKey"]); Assert.Equal("contoso.onmicrosoft.com", result.Metadata["teams.config.tenant"]); Assert.Equal(channel.Config.Endpoint, result.Metadata["teams.config.endpoint"]); using var payload = JsonDocument.Parse(result.Preview.Body); Assert.Equal("message", payload.RootElement.GetProperty("type").GetString()); Assert.Equal(result.Preview.TextBody, payload.RootElement.GetProperty("text").GetString()); Assert.Equal(result.Preview.Summary, payload.RootElement.GetProperty("summary").GetString()); var attachments = payload.RootElement.GetProperty("attachments"); Assert.True(attachments.GetArrayLength() > 0); Assert.Equal( "AdaptiveCard", attachments[0].GetProperty("content").GetProperty("type").GetString()); } [Fact] public async Task BuildPreviewAsync_TruncatesLongFallback() { var provider = new TeamsChannelTestProvider(); var channel = CreateChannel( endpoint: "https://contoso.webhook.office.com/webhookb2/tenant@uuid/IncomingWebhook/abcdef0123456789", properties: new Dictionary()); var longText = new string('A', 600); var request = new ChannelTestPreviewRequest( TargetOverride: null, TemplateId: null, Title: null, Summary: null, Body: null, TextBody: longText, Locale: null, Metadata: new Dictionary(), Attachments: new List()); var context = new ChannelTestPreviewContext( channel.TenantId, channel, channel.Config.Endpoint!, request, DateTimeOffset.UtcNow, TraceId: "trace-teams-002"); var result = await provider.BuildPreviewAsync(context, CancellationToken.None); var metadata = Assert.IsAssignableFrom>(result.Metadata); var fallback = Assert.IsType(result.Preview.TextBody); Assert.Equal(512, fallback.Length); Assert.Equal(fallback, metadata["teams.fallbackText"]); Assert.StartsWith(new string('A', 512), fallback); } private static NotifyChannel CreateChannel(string endpoint, IDictionary properties) { return NotifyChannel.Create( channelId: "channel-teams-sec-ops", tenantId: "tenant-sec", name: "teams:sec-ops", type: NotifyChannelType.Teams, config: NotifyChannelConfig.Create( secretRef: "ref://notify/channels/teams/sec-ops", target: null, endpoint: endpoint, properties: properties)); } private static string ComputeSecretHash(string secretRef) { var bytes = System.Text.Encoding.UTF8.GetBytes(secretRef.Trim()); var hash = System.Security.Cryptography.SHA256.HashData(bytes); return Convert.ToHexString(hash.AsSpan(0, 8)).ToLowerInvariant(); } }