6864890e5f
The review asked for a regression test pinning DTD-prohibited parsing. Writing it showed the premise was wrong: XDocument.Parse permits an internal DTD subset and expands its entities, so a DOCTYPE-bearing response body parsed fine and the guard did not exist (entity-expansion DoS on external input). Parse now goes through XmlReader with DtdProcessing.Prohibit and a null XmlResolver. The regression test feeds a DOCTYPE + ENTITY payload shaped as a well-formed EWS error response, so it fails if DtdProcessing is ever loosened rather than passing for the unrelated-XML reason.
168 lines
7.0 KiB
C#
168 lines
7.0 KiB
C#
using ZB.MOM.WW.ScadaBridge.NotificationService.Ews;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.NotificationService.Tests.Ews;
|
|
|
|
/// <summary>
|
|
/// Tests for the pure EWS response parser. The parser reports shape only
|
|
/// (success / error / SOAP fault / unparseable) plus the response code — the
|
|
/// transient-vs-permanent judgement belongs to the sender's classifier.
|
|
/// </summary>
|
|
public class EwsResponseParserTests
|
|
{
|
|
private const string SuccessBody = """
|
|
<?xml version="1.0" encoding="utf-8"?>
|
|
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
|
|
<s:Body>
|
|
<m:CreateItemResponse xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages"
|
|
xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types">
|
|
<m:ResponseMessages>
|
|
<m:CreateItemResponseMessage ResponseClass="Success">
|
|
<m:ResponseCode>NoError</m:ResponseCode>
|
|
<m:Items><t:Message><t:ItemId Id="AAA=" ChangeKey="CQ=="/></t:Message></m:Items>
|
|
</m:CreateItemResponseMessage>
|
|
</m:ResponseMessages>
|
|
</m:CreateItemResponse>
|
|
</s:Body>
|
|
</s:Envelope>
|
|
""";
|
|
|
|
private const string ErrorBody = """
|
|
<?xml version="1.0" encoding="utf-8"?>
|
|
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
|
|
<s:Body>
|
|
<m:CreateItemResponse xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages"
|
|
xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types">
|
|
<m:ResponseMessages>
|
|
<m:CreateItemResponseMessage ResponseClass="Error">
|
|
<m:MessageText>server busy</m:MessageText>
|
|
<m:ResponseCode>ErrorServerBusy</m:ResponseCode>
|
|
<m:DescriptiveLinkKey>0</m:DescriptiveLinkKey>
|
|
</m:CreateItemResponseMessage>
|
|
</m:ResponseMessages>
|
|
</m:CreateItemResponse>
|
|
</s:Body>
|
|
</s:Envelope>
|
|
""";
|
|
|
|
private const string FaultWithResponseCodeBody = """
|
|
<?xml version="1.0" encoding="utf-8"?>
|
|
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
|
|
<s:Body>
|
|
<s:Fault>
|
|
<faultcode xmlns:a="http://schemas.microsoft.com/exchange/services/2006/types">a:ErrorSchemaValidation</faultcode>
|
|
<faultstring xml:lang="en-US">The request failed schema validation.</faultstring>
|
|
<detail>
|
|
<e:ResponseCode xmlns:e="http://schemas.microsoft.com/exchange/services/2006/errors">ErrorSchemaValidation</e:ResponseCode>
|
|
<e:Message xmlns:e="http://schemas.microsoft.com/exchange/services/2006/errors">The request failed schema validation.</e:Message>
|
|
</detail>
|
|
</s:Fault>
|
|
</s:Body>
|
|
</s:Envelope>
|
|
""";
|
|
|
|
private const string FaultWithoutResponseCodeBody = """
|
|
<?xml version="1.0" encoding="utf-8"?>
|
|
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
|
|
<s:Body>
|
|
<s:Fault>
|
|
<faultcode>s:Server</faultcode>
|
|
<faultstring xml:lang="en-US">An internal server error occurred.</faultstring>
|
|
<detail><e:Message xmlns:e="http://schemas.microsoft.com/exchange/services/2006/errors">boom</e:Message></detail>
|
|
</s:Fault>
|
|
</s:Body>
|
|
</s:Envelope>
|
|
""";
|
|
|
|
[Fact]
|
|
public void Parse_SuccessResponse_ReturnsSuccessWithNoError()
|
|
{
|
|
var result = EwsResponseParser.Parse(SuccessBody);
|
|
|
|
Assert.Equal(EwsResponseKind.Success, result.Kind);
|
|
Assert.Equal("NoError", result.ResponseCode);
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_ErrorResponse_ReturnsErrorWithCodeAndMessage()
|
|
{
|
|
var result = EwsResponseParser.Parse(ErrorBody);
|
|
|
|
Assert.Equal(EwsResponseKind.Error, result.Kind);
|
|
Assert.Equal("ErrorServerBusy", result.ResponseCode);
|
|
Assert.Equal("server busy", result.MessageText);
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_SoapFaultWithDetailResponseCode_ReturnsFaultWithCode()
|
|
{
|
|
var result = EwsResponseParser.Parse(FaultWithResponseCodeBody);
|
|
|
|
Assert.Equal(EwsResponseKind.Fault, result.Kind);
|
|
Assert.Equal("ErrorSchemaValidation", result.ResponseCode);
|
|
Assert.Equal("The request failed schema validation.", result.MessageText);
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_SoapFaultWithoutResponseCode_ReturnsFaultWithNullCode()
|
|
{
|
|
var result = EwsResponseParser.Parse(FaultWithoutResponseCodeBody);
|
|
|
|
Assert.Equal(EwsResponseKind.Fault, result.Kind);
|
|
Assert.Null(result.ResponseCode);
|
|
Assert.Equal("An internal server error occurred.", result.MessageText);
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_PayloadWithDtdEntityDeclaration_IsRejectedAsUnparseable()
|
|
{
|
|
// Pins DTD-prohibited parsing (XXE guard). The response body is external input, so the
|
|
// parser must never process a DOCTYPE. XDocument.Parse prohibits DTDs by default; a
|
|
// refactor that supplies XmlReaderSettings with a looser DtdProcessing fails here.
|
|
//
|
|
// The payload is deliberately a well-formed EWS response so the assertion has teeth: with
|
|
// DTDs prohibited the DOCTYPE itself throws and the body is Unparseable, whereas any
|
|
// DtdProcessing.Parse configuration expands the internal entity and yields Kind=Error.
|
|
// The external file:/// entity is declared alongside it as the XXE payload that must
|
|
// likewise never be reached.
|
|
const string xxe = """
|
|
<?xml version="1.0" encoding="utf-8"?>
|
|
<!DOCTYPE Envelope [
|
|
<!ENTITY xxe SYSTEM "file:///etc/passwd">
|
|
<!ENTITY expanded "server busy">
|
|
]>
|
|
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
|
|
<s:Body>
|
|
<m:CreateItemResponse xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages">
|
|
<m:ResponseMessages>
|
|
<m:CreateItemResponseMessage ResponseClass="Error">
|
|
<m:MessageText>&expanded;</m:MessageText>
|
|
<m:ResponseCode>ErrorServerBusy</m:ResponseCode>
|
|
</m:CreateItemResponseMessage>
|
|
</m:ResponseMessages>
|
|
</m:CreateItemResponse>
|
|
</s:Body>
|
|
</s:Envelope>
|
|
""";
|
|
|
|
var result = EwsResponseParser.Parse(xxe);
|
|
|
|
Assert.Equal(EwsResponseKind.Unparseable, result.Kind);
|
|
Assert.Null(result.ResponseCode);
|
|
Assert.Null(result.MessageText);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("not xml")]
|
|
[InlineData("")]
|
|
[InlineData(" ")]
|
|
[InlineData("<foo/>")]
|
|
public void Parse_GarbageOrUnrelatedPayload_ReturnsUnparseable(string body)
|
|
{
|
|
var result = EwsResponseParser.Parse(body);
|
|
|
|
Assert.Equal(EwsResponseKind.Unparseable, result.Kind);
|
|
Assert.Null(result.ResponseCode);
|
|
Assert.Null(result.MessageText);
|
|
}
|
|
}
|