using System.Text.Json; using Google.Protobuf; using MxGateway.Contracts; using MxGateway.Contracts.Proto; namespace MxGateway.Tests.Contracts; public sealed class ClientBehaviorFixtureTests { private static readonly JsonParser ProtobufJsonParser = new(JsonParser.Settings.Default); [Fact] public void BehaviorManifest_DeclaresCurrentProtocolVersionsAndExistingFixtures() { using JsonDocument manifest = LoadBehaviorManifest(); JsonElement root = manifest.RootElement; Assert.Equal(1, root.GetProperty("schemaVersion").GetInt32()); Assert.Equal("mxaccess-gateway-client-behavior", root.GetProperty("fixtureSet").GetString()); Assert.Equal(GatewayContractInfo.GatewayProtocolVersion, root.GetProperty("gatewayProtocolVersion").GetUInt32()); Assert.Equal(GatewayContractInfo.WorkerProtocolVersion, root.GetProperty("workerProtocolVersion").GetUInt32()); HashSet fixtureIds = new(StringComparer.Ordinal); foreach (JsonElement fixture in root.GetProperty("fixtures").EnumerateArray()) { string id = fixture.GetProperty("id").GetString()!; string path = fixture.GetProperty("path").GetString()!; string category = fixture.GetProperty("category").GetString()!; string messageType = fixture.GetProperty("messageType").GetString()!; Assert.True(fixtureIds.Add(id), $"Duplicate behavior fixture id '{id}'."); Assert.Contains(category, KnownCategories); Assert.Contains(messageType, KnownMessageTypes); Assert.True( File.Exists(Path.Combine(GetBehaviorFixtureRoot().FullName, path)), $"Expected behavior fixture '{path}' to exist."); Assert.False(Path.IsPathRooted(path), $"Fixture path '{path}' must be relative."); Assert.NotEmpty(fixture.GetProperty("expectation").GetString()!); } } [Fact] public void ProtoInputManifest_ReferencesBehaviorFixtureRoot() { DirectoryInfo repositoryRoot = FindRepositoryRoot(); string manifestPath = Path.Combine(repositoryRoot.FullName, "clients", "proto", "proto-inputs.json"); using JsonDocument manifest = JsonDocument.Parse(File.ReadAllText(manifestPath)); string fixtureRoot = manifest.RootElement.GetProperty("behaviorFixtureRoot").GetString()!; Assert.Equal("clients/proto/fixtures/behavior", fixtureRoot); Assert.True(Directory.Exists(Path.Combine(repositoryRoot.FullName, fixtureRoot))); } [Fact] public void CommandReplyFixtures_ParseWithCurrentContractAndPreserveMxAccessDetails() { IReadOnlyList fixtures = LoadManifestFixtures("command_replies"); Assert.NotEmpty(fixtures); foreach (JsonElement fixture in fixtures) { MxCommandReply reply = ParseFixture( fixture, MxCommandReply.Parser); Assert.NotEqual(MxCommandKind.Unspecified, reply.Kind); Assert.NotEqual(ProtocolStatusCode.Unspecified, reply.ProtocolStatus.Code); Assert.True(reply.HasHresult, $"Fixture '{GetFixtureId(fixture)}' must carry an HRESULT."); Assert.NotEmpty(reply.Statuses); Assert.NotEqual(MxDataType.Unspecified, reply.ReturnValue.DataType); Assert.True( reply.ReturnValue.KindCase != MxValue.KindOneofCase.None || reply.ReturnValue.IsNull, $"Fixture '{GetFixtureId(fixture)}' must carry a typed value, raw value, or explicit null."); } MxCommandReply failedWrite = ParseFixture( Assert.Single(fixtures, fixture => GetFixtureId(fixture) == "command-reply.write.mxaccess-failure"), MxCommandReply.Parser); Assert.Equal(ProtocolStatusCode.MxaccessFailure, failedWrite.ProtocolStatus.Code); Assert.Equal(-2147220992, failedWrite.Hresult); Assert.True(failedWrite.Statuses.Count > 1); Assert.All(failedWrite.Statuses, status => Assert.Equal(0, status.Success)); } [Fact] public void EventStreamFixtures_ParseWithMonotonicSequencesAndExpectedFamilies() { IReadOnlyList fixtures = LoadManifestFixtures("event_streams"); Assert.NotEmpty(fixtures); foreach (JsonElement fixture in fixtures) { using JsonDocument document = JsonDocument.Parse(File.ReadAllText(GetFixturePath(fixture))); ulong previousSequence = 0; List families = []; foreach (JsonElement eventElement in document.RootElement.GetProperty("events").EnumerateArray()) { MxEvent gatewayEvent = ProtobufJsonParser.Parse(eventElement.GetRawText()); Assert.True(gatewayEvent.WorkerSequence > previousSequence); Assert.Equal(document.RootElement.GetProperty("sessionId").GetString(), gatewayEvent.SessionId); Assert.NotEmpty(gatewayEvent.Statuses); AssertEventBodyMatchesFamily(gatewayEvent); previousSequence = gatewayEvent.WorkerSequence; families.Add(gatewayEvent.Family); } Assert.Contains(MxEventFamily.OnDataChange, families); Assert.Contains(MxEventFamily.OnWriteComplete, families); Assert.Contains(MxEventFamily.OperationComplete, families); Assert.Contains(MxEventFamily.OnBufferedDataChange, families); } } [Fact] public void ValueConversionFixtures_ParseTypedValuesAndRawFallbacks() { JsonElement cases = LoadCaseSet("value_conversion", "cases"); bool sawRawFallback = false; bool sawRawArrayFallback = false; bool sawTypedArray = false; foreach (JsonElement valueCase in cases.EnumerateArray()) { MxValue value = ProtobufJsonParser.Parse( valueCase.GetProperty("value").GetRawText()); string expectedKind = valueCase.GetProperty("expectedKind").GetString()!; Assert.NotEqual(MxDataType.Unspecified, value.DataType); AssertJsonKindMatchesValueKind(expectedKind, value); sawRawFallback |= value.KindCase == MxValue.KindOneofCase.RawValue && !string.IsNullOrWhiteSpace(value.RawDiagnostic) && value.RawDataType != 0; sawRawArrayFallback |= value.KindCase == MxValue.KindOneofCase.ArrayValue && value.ArrayValue.ValuesCase == MxArray.ValuesOneofCase.RawValues && !string.IsNullOrWhiteSpace(value.ArrayValue.RawDiagnostic) && value.ArrayValue.RawElementDataType != 0; sawTypedArray |= value.KindCase == MxValue.KindOneofCase.ArrayValue && value.ArrayValue.ValuesCase != MxArray.ValuesOneofCase.RawValues; } Assert.True(sawRawFallback, "Expected at least one raw scalar fallback case."); Assert.True(sawRawArrayFallback, "Expected at least one raw array fallback case."); Assert.True(sawTypedArray, "Expected at least one typed array case."); } [Fact] public void StatusConversionFixtures_ParseStatusArraysAndRawFields() { JsonElement cases = LoadCaseSet("status_conversion", "cases"); bool sawRawUnknown = false; foreach (JsonElement statusCase in cases.EnumerateArray()) { MxStatusProxy status = ProtobufJsonParser.Parse( statusCase.GetProperty("status").GetRawText()); Assert.NotEqual(MxStatusCategory.Unspecified, status.Category); Assert.NotEqual(MxStatusSource.Unspecified, status.DetectedBy); Assert.NotEmpty(status.DiagnosticText); sawRawUnknown |= status.Category == MxStatusCategory.Unknown && status.RawCategory != 0 && status.RawDetectedBy != 0; } Assert.True(sawRawUnknown, "Expected a status case with unknown raw native fields."); } [Fact] public void AuthErrorFixtures_MapAuthenticationAuthorizationAndRedactCredentials() { JsonElement cases = LoadCaseSet("auth_errors", "cases"); HashSet statusCodes = new(StringComparer.Ordinal); foreach (JsonElement authCase in cases.EnumerateArray()) { string grpcStatusCode = authCase.GetProperty("grpcStatusCode").GetString()!; string category = authCase.GetProperty("clientErrorCategory").GetString()!; string redactedOutput = authCase.GetProperty("expectedRedactedOutput").GetString()!; string serialized = authCase.GetRawText(); Assert.Contains(grpcStatusCode, AuthGrpcStatusCodes); Assert.Contains(category, AuthClientErrorCategories); string authorization = authCase.GetProperty("inputMetadata").GetProperty("authorization").GetString()!; if (!string.IsNullOrEmpty(authorization)) { Assert.Contains("", serialized); } Assert.DoesNotContain("mxgw_", serialized, StringComparison.Ordinal); Assert.DoesNotContain("secret", redactedOutput, StringComparison.OrdinalIgnoreCase); statusCodes.Add(grpcStatusCode); } Assert.Contains("UNAUTHENTICATED", statusCodes); Assert.Contains("PERMISSION_DENIED", statusCodes); } [Fact] public void TimeoutCancelFixtures_DocumentClientWaitAndWorkerCommandBehavior() { JsonElement cases = LoadCaseSet("timeout_cancel", "cases"); HashSet statusCodes = new(StringComparer.Ordinal); foreach (JsonElement timeoutCase in cases.EnumerateArray()) { string grpcStatusCode = timeoutCase.GetProperty("grpcStatusCode").GetString()!; Assert.Contains(grpcStatusCode, TimeoutGrpcStatusCodes); Assert.NotEmpty(timeoutCase.GetProperty("clientDeadline").GetString()!); Assert.NotEmpty(timeoutCase.GetProperty("gatewayWaitBehavior").GetString()!); Assert.NotEmpty(timeoutCase.GetProperty("workerCommandBehavior").GetString()!); Assert.NotEmpty(timeoutCase.GetProperty("expectedClientAction").GetString()!); statusCodes.Add(grpcStatusCode); } Assert.Contains("DEADLINE_EXCEEDED", statusCodes); Assert.Contains("CANCELLED", statusCodes); } private static readonly string[] KnownCategories = [ "command_replies", "event_streams", "value_conversion", "status_conversion", "auth_errors", "timeout_cancel", ]; private static readonly string[] KnownMessageTypes = [ "mxaccess_gateway.v1.MxCommandReply", "mxaccess_gateway.v1.MxEvent", "mxaccess_gateway.v1.MxValue", "mxaccess_gateway.v1.MxStatusProxy", "client_behavior.v1.AuthErrorCase", "client_behavior.v1.TimeoutCancelCase", ]; private static readonly string[] AuthGrpcStatusCodes = [ "UNAUTHENTICATED", "PERMISSION_DENIED", ]; private static readonly string[] AuthClientErrorCategories = [ "AuthenticationError", "AuthorizationError", ]; private static readonly string[] TimeoutGrpcStatusCodes = [ "DEADLINE_EXCEEDED", "CANCELLED", ]; private static T ParseFixture( JsonElement fixture, MessageParser parser) where T : IMessage { return parser.ParseJson(File.ReadAllText(GetFixturePath(fixture))); } private static JsonElement LoadCaseSet( string category, string propertyName) { JsonElement fixture = Assert.Single(LoadManifestFixtures(category)); using JsonDocument document = JsonDocument.Parse(File.ReadAllText(GetFixturePath(fixture))); return document.RootElement.GetProperty(propertyName).Clone(); } private static IReadOnlyList LoadManifestFixtures(string category) { using JsonDocument manifest = LoadBehaviorManifest(); return manifest.RootElement .GetProperty("fixtures") .EnumerateArray() .Where(fixture => fixture.GetProperty("category").GetString() == category) .Select(fixture => fixture.Clone()) .ToArray(); } private static JsonDocument LoadBehaviorManifest() { return JsonDocument.Parse(File.ReadAllText(Path.Combine(GetBehaviorFixtureRoot().FullName, "manifest.json"))); } private static string GetFixturePath(JsonElement fixture) { return Path.Combine(GetBehaviorFixtureRoot().FullName, fixture.GetProperty("path").GetString()!); } private static string GetFixtureId(JsonElement fixture) { return fixture.GetProperty("id").GetString()!; } private static DirectoryInfo GetBehaviorFixtureRoot() { DirectoryInfo repositoryRoot = FindRepositoryRoot(); return new DirectoryInfo(Path.Combine(repositoryRoot.FullName, "clients", "proto", "fixtures", "behavior")); } private static DirectoryInfo FindRepositoryRoot() { DirectoryInfo? current = new(AppContext.BaseDirectory); while (current is not null) { if (File.Exists(Path.Combine(current.FullName, "AGENTS.md")) && Directory.Exists(Path.Combine(current.FullName, "src")) && Directory.Exists(Path.Combine(current.FullName, "clients"))) { return current; } current = current.Parent; } throw new DirectoryNotFoundException("Could not locate the repository root from the test output directory."); } private static void AssertEventBodyMatchesFamily(MxEvent gatewayEvent) { switch (gatewayEvent.Family) { case MxEventFamily.OnDataChange: Assert.Equal(MxEvent.BodyOneofCase.OnDataChange, gatewayEvent.BodyCase); break; case MxEventFamily.OnWriteComplete: Assert.Equal(MxEvent.BodyOneofCase.OnWriteComplete, gatewayEvent.BodyCase); break; case MxEventFamily.OperationComplete: Assert.Equal(MxEvent.BodyOneofCase.OperationComplete, gatewayEvent.BodyCase); break; case MxEventFamily.OnBufferedDataChange: Assert.Equal(MxEvent.BodyOneofCase.OnBufferedDataChange, gatewayEvent.BodyCase); break; default: throw new InvalidOperationException($"Unexpected event family '{gatewayEvent.Family}'."); } } private static void AssertJsonKindMatchesValueKind( string expectedKind, MxValue value) { MxValue.KindOneofCase expected = expectedKind switch { "boolValue" => MxValue.KindOneofCase.BoolValue, "int32Value" => MxValue.KindOneofCase.Int32Value, "int64Value" => MxValue.KindOneofCase.Int64Value, "floatValue" => MxValue.KindOneofCase.FloatValue, "doubleValue" => MxValue.KindOneofCase.DoubleValue, "stringValue" => MxValue.KindOneofCase.StringValue, "timestampValue" => MxValue.KindOneofCase.TimestampValue, "arrayValue" => MxValue.KindOneofCase.ArrayValue, "rawValue" => MxValue.KindOneofCase.RawValue, _ => throw new InvalidOperationException($"Unexpected expected value kind '{expectedKind}'."), }; Assert.Equal(expected, value.KindCase); } }