using System.Xml.Linq; using ZB.MOM.WW.ScadaBridge.NotificationService.Ews; namespace ZB.MOM.WW.ScadaBridge.NotificationService.Tests.Ews; /// /// Tests for the pure EWS CreateItem SOAP envelope builder: the fixed schema /// sequence Exchange validates against, BCC-only recipients, and XML escaping of /// operator-authored subject/body content. /// public class EwsSoapEnvelopeTests { private static readonly XNamespace Soap = "http://schemas.xmlsoap.org/soap/envelope/"; private static readonly XNamespace Types = "http://schemas.microsoft.com/exchange/services/2006/types"; private static readonly XNamespace Messages = "http://schemas.microsoft.com/exchange/services/2006/messages"; private static XDocument Build( string from = "scada@example.com", IReadOnlyList? bcc = null, string subject = "Subject", string body = "Body") => XDocument.Parse(EwsSoapEnvelope.BuildCreateItem(from, bcc ?? ["one@example.com"], subject, body)); [Fact] public void BuildCreateItem_SetsSendOnlyDispositionAndExchange2013RequestVersion() { var doc = Build(); var createItem = doc.Descendants(Messages + "CreateItem").Single(); Assert.Equal("SendOnly", (string?)createItem.Attribute("MessageDisposition")); var version = doc.Descendants(Types + "RequestServerVersion").Single(); Assert.Equal("Exchange2013", (string?)version.Attribute("Version")); Assert.Single(doc.Descendants(Soap + "Header").Single().Elements()); } [Fact] public void BuildCreateItem_EscapesMarkupInSubjectAndBody() { const string subject = "Alarm & \"critical\""; const string body = "value < 5 & tag=\"A\" > threshold"; var doc = Build(subject: subject, body: body); // Round-tripping through the parser proves the content was escaped, not concatenated. Assert.Equal(subject, doc.Descendants(Types + "Subject").Single().Value); Assert.Equal(body, doc.Descendants(Types + "Body").Single().Value); Assert.Equal("Text", (string?)doc.Descendants(Types + "Body").Single().Attribute("BodyType")); } [Fact] public void BuildCreateItem_PutsEveryRecipientInBccAndNeverEmitsToRecipients() { string[] recipients = ["a@example.com", "b@example.com", "c@example.com"]; var doc = Build(bcc: recipients); var bcc = doc.Descendants(Types + "BccRecipients").Single(); var addresses = bcc.Elements(Types + "Mailbox") .Select(m => m.Element(Types + "EmailAddress")!.Value) .ToArray(); Assert.Equal(recipients, addresses); // Recipient privacy is the whole reason for BCC-only: no To/Cc may leak the list. Assert.DoesNotContain(doc.Descendants(), e => e.Name.LocalName is "ToRecipients" or "CcRecipients"); } [Fact] public void BuildCreateItem_EmitsFromMailbox() { var doc = Build(from: "scada-alerts@example.com"); var from = doc.Descendants(Types + "From").Single(); var mailbox = from.Elements(Types + "Mailbox").Single(); Assert.Equal("scada-alerts@example.com", mailbox.Element(Types + "EmailAddress")!.Value); } [Fact] public void BuildCreateItem_OrdersMessageChildrenPerEwsSchemaSequence() { var doc = Build(); // MessageType is an xs:sequence — any other order is ErrorSchemaValidation at the server. var message = doc.Descendants(Types + "Message").Single(); Assert.Equal( new[] { "Subject", "Body", "BccRecipients", "From" }, message.Elements().Select(e => e.Name.LocalName).ToArray()); } [Fact] public void BuildCreateItem_StartsWithUtf8XmlDeclaration() { var xml = EwsSoapEnvelope.BuildCreateItem("a@example.com", ["b@example.com"], "s", "b"); Assert.StartsWith("", StringComparison.Ordinal)]; Assert.Contains("utf-8", declaration, StringComparison.OrdinalIgnoreCase); } }