f40409a688
Tracking IDs are review bookkeeping that rots in shipped source; the rationale they tagged is kept. Comment-only change (CommentChecker 458 -> 16 false positives).
171 lines
7.7 KiB
C#
171 lines
7.7 KiB
C#
using Google.Protobuf;
|
|
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
|
|
|
namespace ZB.MOM.WW.MxGateway.Client.Tests;
|
|
|
|
/// <summary>
|
|
/// Tests for the credential-scrub and malformed-reply contracts on the
|
|
/// credential and id-returning session helpers, driven from shared behavior fixtures.
|
|
/// </summary>
|
|
public sealed class MxGatewaySessionReplyContractTests
|
|
{
|
|
/// <summary>
|
|
/// Verifies that when MXAccess echoes the submitted credential back in its failure diagnostic,
|
|
/// the surfaced exception message must scrub it to the library redaction marker.
|
|
/// </summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task AuthenticateUserAsync_RedactsEchoedCredentialInFailureMessage()
|
|
{
|
|
const string password = "sup3rSecretVerify9f3a2b";
|
|
FakeGatewayTransport transport = CreateTransport();
|
|
transport.AddInvokeReply(ReadReplyFixture("authenticate-user.echoed-credential.reply.json"));
|
|
await using MxGatewayClient client = CreateClient(transport);
|
|
MxGatewaySession session = await client.OpenSessionAsync();
|
|
|
|
MxAccessException exception = await Assert.ThrowsAsync<MxAccessException>(
|
|
async () => await session.AuthenticateUserAsync(12, "operator", password));
|
|
|
|
Assert.DoesNotContain(password, exception.Message, StringComparison.Ordinal);
|
|
Assert.Contains("<redacted>", exception.Message, StringComparison.Ordinal);
|
|
// ToString() is what logging frameworks emit; the secret-bearing original must not be
|
|
// chained as an inner exception where it would re-surface the credential verbatim.
|
|
Assert.DoesNotContain(password, exception.ToString(), StringComparison.Ordinal);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that the redacted exception must not leak the echoed credential through any structured
|
|
/// accessor either — <see cref="MxAccessException.Reply"/> (protocol message, diagnostic
|
|
/// message, and each MXSTATUS_PROXY diagnostic text) and <see cref="MxGatewayException.Statuses"/>
|
|
/// all carry the server-echoed credential verbatim before the fix. Both the OK+negative-HRESULT
|
|
/// and the MXACCESS_FAILURE reply route to <see cref="MxAccessException"/>, so both must scrub.
|
|
/// </summary>
|
|
/// <param name="fixture">The echoed-credential reply fixture to drive.</param>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Theory]
|
|
[InlineData("authenticate-user.echoed-credential.reply.json")]
|
|
[InlineData("authenticate-user.echoed-credential-mxaccess-failure.reply.json")]
|
|
public async Task AuthenticateUserAsync_RedactsEchoedCredentialInStructuredAccessors(string fixture)
|
|
{
|
|
const string password = "sup3rSecretVerify9f3a2b";
|
|
FakeGatewayTransport transport = CreateTransport();
|
|
transport.AddInvokeReply(ReadReplyFixture(fixture));
|
|
await using MxGatewayClient client = CreateClient(transport);
|
|
MxGatewaySession session = await client.OpenSessionAsync();
|
|
|
|
MxAccessException exception = await Assert.ThrowsAsync<MxAccessException>(
|
|
async () => await session.AuthenticateUserAsync(12, "operator", password));
|
|
|
|
Assert.DoesNotContain(password, exception.Message, StringComparison.Ordinal);
|
|
Assert.Contains("<redacted>", exception.Message, StringComparison.Ordinal);
|
|
Assert.DoesNotContain(password, exception.ToString(), StringComparison.Ordinal);
|
|
Assert.DoesNotContain(password, exception.Reply.ProtocolStatus.Message, StringComparison.Ordinal);
|
|
Assert.DoesNotContain(password, exception.Reply.DiagnosticMessage, StringComparison.Ordinal);
|
|
foreach (MxStatusProxy status in exception.Reply.Statuses)
|
|
{
|
|
Assert.DoesNotContain(password, status.DiagnosticText, StringComparison.Ordinal);
|
|
}
|
|
|
|
foreach (MxStatusProxy status in exception.Statuses)
|
|
{
|
|
Assert.DoesNotContain(password, status.DiagnosticText, StringComparison.Ordinal);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that an OK reply that carries neither the typed AuthenticateUser payload nor an
|
|
/// int32 return_value is a malformed reply, surfaced as a typed exception rather than an NRE.
|
|
/// </summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task AuthenticateUserAsync_MissingPayloadAndReturnValue_ThrowsMalformedReply()
|
|
{
|
|
FakeGatewayTransport transport = CreateTransport();
|
|
transport.AddInvokeReply(ReadReplyFixture("authenticate-user.missing-payload.reply.json"));
|
|
await using MxGatewayClient client = CreateClient(transport);
|
|
MxGatewaySession session = await client.OpenSessionAsync();
|
|
|
|
await Assert.ThrowsAsync<MxGatewayMalformedReplyException>(
|
|
async () => await session.AuthenticateUserAsync(12, "operator", "pw"));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that an OK reply that omits the typed payload but carries an int32 return_value
|
|
/// resolves to that return value.
|
|
/// </summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task AuthenticateUserAsync_ReturnValueOnly_ResolvesReturnValue()
|
|
{
|
|
FakeGatewayTransport transport = CreateTransport();
|
|
transport.AddInvokeReply(ReadReplyFixture("authenticate-user.return-value-only.reply.json"));
|
|
await using MxGatewayClient client = CreateClient(transport);
|
|
MxGatewaySession session = await client.OpenSessionAsync();
|
|
|
|
int userId = await session.AuthenticateUserAsync(12, "operator", "pw");
|
|
|
|
Assert.Equal(7, userId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that the AddBufferedItem fallback shares the malformed-reply contract — an OK reply
|
|
/// with neither a typed item handle nor an int32 return_value throws the typed exception.
|
|
/// </summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task AddBufferedItemAsync_MissingPayloadAndReturnValue_ThrowsMalformedReply()
|
|
{
|
|
FakeGatewayTransport transport = CreateTransport();
|
|
transport.AddInvokeReply(new MxCommandReply
|
|
{
|
|
SessionId = "session-fixture",
|
|
Kind = MxCommandKind.AddBufferedItem,
|
|
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
|
|
});
|
|
await using MxGatewayClient client = CreateClient(transport);
|
|
MxGatewaySession session = await client.OpenSessionAsync();
|
|
|
|
await Assert.ThrowsAsync<MxGatewayMalformedReplyException>(
|
|
async () => await session.AddBufferedItemAsync(12, "Area001.Pump001.Speed", "runtime"));
|
|
}
|
|
|
|
private static MxGatewayClient CreateClient(FakeGatewayTransport transport)
|
|
{
|
|
return new MxGatewayClient(transport.Options, transport);
|
|
}
|
|
|
|
private static FakeGatewayTransport CreateTransport()
|
|
{
|
|
return new FakeGatewayTransport(new MxGatewayClientOptions
|
|
{
|
|
Endpoint = new Uri("http://localhost:5000"),
|
|
ApiKey = "test-api-key",
|
|
});
|
|
}
|
|
|
|
private static MxCommandReply ReadReplyFixture(string fileName)
|
|
{
|
|
DirectoryInfo directory = new(AppContext.BaseDirectory);
|
|
while (directory is not null)
|
|
{
|
|
string path = Path.Combine(
|
|
directory.FullName,
|
|
"clients",
|
|
"proto",
|
|
"fixtures",
|
|
"behavior",
|
|
"command-replies",
|
|
fileName);
|
|
|
|
if (File.Exists(path))
|
|
{
|
|
return JsonParser.Default.Parse<MxCommandReply>(File.ReadAllText(path));
|
|
}
|
|
|
|
directory = directory.Parent!;
|
|
}
|
|
|
|
throw new FileNotFoundException(fileName);
|
|
}
|
|
}
|