using System.Reflection; using System.Text.Json; namespace ScadaLink.Commons.Tests.Messages; public class MessageConventionTests { private static readonly Assembly CommonsAssembly = typeof(ScadaLink.Commons.Types.RetryPolicy).Assembly; private static IEnumerable GetMessageTypes() => CommonsAssembly.GetTypes() .Where(t => t.Namespace != null && t.Namespace.Contains(".Messages.") && !t.IsEnum && !t.IsInterface && (t.IsClass || (t.IsValueType && !t.IsPrimitive))); [Fact] public void AllMessageTypes_ShouldBeRecords() { foreach (var type in GetMessageTypes()) { // Records have a compiler-generated $ method var cloneMethod = type.GetMethod("$", BindingFlags.Public | BindingFlags.Instance); Assert.True(cloneMethod != null, $"{type.FullName} in Messages namespace should be a record type"); } } [Fact] public void AllMessageTimestampProperties_ShouldBeDateTimeOffset() { foreach (var type in GetMessageTypes()) { foreach (var prop in type.GetProperties()) { if (prop.Name.Contains("Timestamp") || prop.Name == "GeneratedAt" || prop.Name == "DeployedAt") { var underlyingType = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType; Assert.True(underlyingType == typeof(DateTimeOffset), $"{type.Name}.{prop.Name} should be DateTimeOffset but is {prop.PropertyType.Name}"); } } } } [Fact] public void JsonRoundTrip_DeployInstanceCommand_ShouldSucceed() { var msg = new ScadaLink.Commons.Messages.Deployment.DeployInstanceCommand( "dep-1", "instance-1", "abc123", "{}", "admin", DateTimeOffset.UtcNow); var json = JsonSerializer.Serialize(msg); var deserialized = JsonSerializer.Deserialize(json); Assert.NotNull(deserialized); Assert.Equal(msg.DeploymentId, deserialized!.DeploymentId); Assert.Equal(msg.InstanceUniqueName, deserialized.InstanceUniqueName); } [Fact] public void JsonForwardCompatibility_UnknownField_ShouldDeserialize() { // Simulate a newer version with an extra field var json = """{"DeploymentId":"dep-1","InstanceUniqueName":"inst-1","RevisionHash":"abc","FlattenedConfigurationJson":"{}","DeployedBy":"admin","Timestamp":"2025-01-01T00:00:00+00:00","NewField":"extra"}"""; var deserialized = JsonSerializer.Deserialize(json); Assert.NotNull(deserialized); Assert.Equal("dep-1", deserialized!.DeploymentId); } [Fact] public void JsonBackwardCompatibility_MissingOptionalField_ShouldDeserialize() { // DeploymentStatusResponse has nullable ErrorMessage var json = """{"DeploymentId":"dep-1","InstanceUniqueName":"inst-1","Status":2,"Timestamp":"2025-01-01T00:00:00+00:00"}"""; var deserialized = JsonSerializer.Deserialize(json); Assert.NotNull(deserialized); Assert.Equal("dep-1", deserialized!.DeploymentId); Assert.Null(deserialized.ErrorMessage); } [Fact] public void JsonRoundTrip_SiteHealthReport_ShouldSucceed() { var msg = new ScadaLink.Commons.Messages.Health.SiteHealthReport( "site-1", 1, DateTimeOffset.UtcNow, new Dictionary { ["conn1"] = ScadaLink.Commons.Types.Enums.ConnectionHealth.Connected }, new Dictionary { ["conn1"] = new(10, 8) }, 0, 0, new Dictionary { ["queue1"] = 5 }, 0, 0, 0, 0); var json = JsonSerializer.Serialize(msg); var deserialized = JsonSerializer.Deserialize(json); Assert.NotNull(deserialized); Assert.Equal("site-1", deserialized!.SiteId); Assert.Equal(1, deserialized.SequenceNumber); } [Fact] public void JsonRoundTrip_DeployArtifactsCommand_ShouldSucceed() { var msg = new ScadaLink.Commons.Messages.Artifacts.DeployArtifactsCommand( "dep-1", new List { new("script1", "code", null, null) }, null, null, null, null, null, DateTimeOffset.UtcNow); var json = JsonSerializer.Serialize(msg); var deserialized = JsonSerializer.Deserialize(json); Assert.NotNull(deserialized); Assert.Equal("dep-1", deserialized!.DeploymentId); Assert.Single(deserialized.SharedScripts!); } }