Add TimeSpan? MinTimeBetweenRuns to TemplateScriptDto and int MaxRetries / TimeSpan RetryDelay to ExternalSystemDto; wire both directions in EntitySerializer. Extends the existing script round-trip assertion and adds Roundtrip_external_system_preserves_retry_config.
213 lines
8.2 KiB
C#
213 lines
8.2 KiB
C#
using ScadaLink.Commons.Entities.ExternalSystems;
|
|
using ScadaLink.Commons.Entities.InboundApi;
|
|
using ScadaLink.Commons.Entities.Notifications;
|
|
using ScadaLink.Commons.Entities.Scripts;
|
|
using ScadaLink.Commons.Entities.Templates;
|
|
using ScadaLink.Commons.Types.Enums;
|
|
using ScadaLink.Transport.Serialization;
|
|
|
|
namespace ScadaLink.Transport.Tests.Serialization;
|
|
|
|
public sealed class EntitySerializerTests
|
|
{
|
|
private static EntityAggregate MakeEmptyAggregate() => new(
|
|
TemplateFolders: Array.Empty<TemplateFolder>(),
|
|
Templates: Array.Empty<Template>(),
|
|
SharedScripts: Array.Empty<SharedScript>(),
|
|
ExternalSystems: Array.Empty<ExternalSystemDefinition>(),
|
|
ExternalSystemMethods: Array.Empty<ExternalSystemMethod>(),
|
|
DatabaseConnections: Array.Empty<DatabaseConnectionDefinition>(),
|
|
NotificationLists: Array.Empty<NotificationList>(),
|
|
SmtpConfigurations: Array.Empty<SmtpConfiguration>(),
|
|
ApiKeys: Array.Empty<ApiKey>(),
|
|
ApiMethods: Array.Empty<ApiMethod>());
|
|
|
|
[Fact]
|
|
public void ToDto_carves_external_system_credentials_into_secrets_block()
|
|
{
|
|
var sys = new ExternalSystemDefinition("erp", "https://erp/api", "ApiKey")
|
|
{
|
|
Id = 1,
|
|
AuthConfiguration = "{\"apiKey\":\"super-secret\"}",
|
|
};
|
|
var aggregate = MakeEmptyAggregate() with { ExternalSystems = new[] { sys } };
|
|
|
|
var sut = new EntitySerializer();
|
|
var dto = sut.ToBundleContent(aggregate);
|
|
|
|
var dtoSys = Assert.Single(dto.ExternalSystems);
|
|
Assert.NotNull(dtoSys.Secrets);
|
|
Assert.True(dtoSys.Secrets!.Values.ContainsKey("AuthConfiguration"));
|
|
Assert.Equal("{\"apiKey\":\"super-secret\"}", dtoSys.Secrets.Values["AuthConfiguration"]);
|
|
// Public part does not carry the secret.
|
|
Assert.Equal("erp", dtoSys.Name);
|
|
Assert.Equal("https://erp/api", dtoSys.BaseUrl);
|
|
Assert.Equal("ApiKey", dtoSys.AuthType);
|
|
}
|
|
|
|
[Fact]
|
|
public void ToDto_carves_smtp_password_into_secrets_block()
|
|
{
|
|
var smtp = new SmtpConfiguration("smtp.example.com", "Basic", "noreply@example.com")
|
|
{
|
|
Id = 1,
|
|
Port = 587,
|
|
Credentials = "user:p@ssw0rd",
|
|
};
|
|
var aggregate = MakeEmptyAggregate() with { SmtpConfigurations = new[] { smtp } };
|
|
|
|
var dto = new EntitySerializer().ToBundleContent(aggregate);
|
|
|
|
var dtoSmtp = Assert.Single(dto.SmtpConfigs);
|
|
Assert.NotNull(dtoSmtp.Secrets);
|
|
Assert.Equal("user:p@ssw0rd", dtoSmtp.Secrets!.Values["Credentials"]);
|
|
Assert.Equal("smtp.example.com", dtoSmtp.Host);
|
|
Assert.Equal(587, dtoSmtp.Port);
|
|
}
|
|
|
|
[Fact]
|
|
public void Roundtrip_template_preserves_attributes_alarms_scripts_composition()
|
|
{
|
|
var folder = new TemplateFolder("root") { Id = 1, SortOrder = 0 };
|
|
var basic = new Template("Basic") { Id = 1, FolderId = 1, Description = "base" };
|
|
basic.Attributes.Add(new TemplateAttribute("Pressure")
|
|
{
|
|
Id = 1,
|
|
TemplateId = 1,
|
|
DataType = DataType.Double,
|
|
Value = "0",
|
|
IsLocked = true,
|
|
Description = "PSI",
|
|
});
|
|
basic.Alarms.Add(new TemplateAlarm("High")
|
|
{
|
|
Id = 1,
|
|
TemplateId = 1,
|
|
PriorityLevel = 2,
|
|
TriggerType = AlarmTriggerType.RangeViolation,
|
|
TriggerConfiguration = "{\"threshold\":100}",
|
|
IsLocked = false,
|
|
});
|
|
basic.Scripts.Add(new TemplateScript("OnUpdate", "return 1;")
|
|
{
|
|
Id = 1,
|
|
TemplateId = 1,
|
|
TriggerType = "Periodic",
|
|
ParameterDefinitions = "[]",
|
|
ReturnDefinition = "void",
|
|
IsLocked = false,
|
|
MinTimeBetweenRuns = TimeSpan.FromSeconds(30),
|
|
});
|
|
|
|
var assembly = new Template("Assembly") { Id = 2, FolderId = 1 };
|
|
assembly.Compositions.Add(new TemplateComposition("MotorA")
|
|
{
|
|
Id = 1,
|
|
TemplateId = 2,
|
|
ComposedTemplateId = 1, // refers to "Basic".
|
|
});
|
|
|
|
var aggregate = MakeEmptyAggregate() with
|
|
{
|
|
TemplateFolders = new[] { folder },
|
|
Templates = new[] { basic, assembly },
|
|
};
|
|
|
|
var sut = new EntitySerializer();
|
|
var dto = sut.ToBundleContent(aggregate);
|
|
var roundTripped = sut.FromBundleContent(dto);
|
|
|
|
var rtBasic = Assert.Single(roundTripped.Templates, t => t.Name == "Basic");
|
|
var rtAttr = Assert.Single(rtBasic.Attributes);
|
|
Assert.Equal("Pressure", rtAttr.Name);
|
|
Assert.Equal(DataType.Double, rtAttr.DataType);
|
|
Assert.Equal("0", rtAttr.Value);
|
|
Assert.True(rtAttr.IsLocked);
|
|
|
|
var rtAlarm = Assert.Single(rtBasic.Alarms);
|
|
Assert.Equal("High", rtAlarm.Name);
|
|
Assert.Equal(AlarmTriggerType.RangeViolation, rtAlarm.TriggerType);
|
|
Assert.Equal("{\"threshold\":100}", rtAlarm.TriggerConfiguration);
|
|
Assert.Equal(2, rtAlarm.PriorityLevel);
|
|
|
|
var rtScript = Assert.Single(rtBasic.Scripts);
|
|
Assert.Equal("OnUpdate", rtScript.Name);
|
|
Assert.Equal("return 1;", rtScript.Code);
|
|
Assert.Equal("Periodic", rtScript.TriggerType);
|
|
Assert.Equal(TimeSpan.FromSeconds(30), rtScript.MinTimeBetweenRuns);
|
|
|
|
var rtAssembly = Assert.Single(roundTripped.Templates, t => t.Name == "Assembly");
|
|
var rtComp = Assert.Single(rtAssembly.Compositions);
|
|
Assert.Equal("MotorA", rtComp.InstanceName);
|
|
}
|
|
|
|
[Fact]
|
|
public void Roundtrip_template_folder_preserves_hierarchy()
|
|
{
|
|
var root = new TemplateFolder("Root") { Id = 1, SortOrder = 0 };
|
|
var child = new TemplateFolder("Pumps") { Id = 2, ParentFolderId = 1, SortOrder = 1 };
|
|
|
|
var aggregate = MakeEmptyAggregate() with
|
|
{
|
|
TemplateFolders = new[] { root, child },
|
|
};
|
|
|
|
var sut = new EntitySerializer();
|
|
var dto = sut.ToBundleContent(aggregate);
|
|
var rt = sut.FromBundleContent(dto);
|
|
|
|
Assert.Equal(2, rt.TemplateFolders.Count);
|
|
var rtRoot = Assert.Single(rt.TemplateFolders, f => f.Name == "Root");
|
|
var rtChild = Assert.Single(rt.TemplateFolders, f => f.Name == "Pumps");
|
|
Assert.Null(rtRoot.ParentFolderId);
|
|
// Hierarchy is preserved by name reference; new local ids get assigned but
|
|
// the child's parent must still point at the row whose name is "Root".
|
|
Assert.NotNull(rtChild.ParentFolderId);
|
|
Assert.Equal(rtRoot.Id, rtChild.ParentFolderId);
|
|
}
|
|
|
|
[Fact]
|
|
public void Roundtrip_external_system_preserves_retry_config()
|
|
{
|
|
var sys = new ExternalSystemDefinition("billing", "https://billing/api", "Basic")
|
|
{
|
|
Id = 1,
|
|
MaxRetries = 5,
|
|
RetryDelay = TimeSpan.FromSeconds(15),
|
|
};
|
|
var aggregate = MakeEmptyAggregate() with { ExternalSystems = new[] { sys } };
|
|
|
|
var sut = new EntitySerializer();
|
|
var dto = sut.ToBundleContent(aggregate);
|
|
var roundTripped = sut.FromBundleContent(dto);
|
|
|
|
var rtSys = Assert.Single(roundTripped.ExternalSystems);
|
|
Assert.Equal("billing", rtSys.Name);
|
|
Assert.Equal(5, rtSys.MaxRetries);
|
|
Assert.Equal(TimeSpan.FromSeconds(15), rtSys.RetryDelay);
|
|
}
|
|
|
|
[Fact]
|
|
public void FromDto_with_null_SecretsBlock_yields_entity_with_default_empty_secrets()
|
|
{
|
|
var dto = new BundleContentDto(
|
|
TemplateFolders: Array.Empty<TemplateFolderDto>(),
|
|
Templates: Array.Empty<TemplateDto>(),
|
|
SharedScripts: Array.Empty<SharedScriptDto>(),
|
|
ExternalSystems: new[]
|
|
{
|
|
new ExternalSystemDto("erp", "https://x", "None", MaxRetries: 3, RetryDelay: TimeSpan.FromSeconds(5), Array.Empty<ExternalSystemMethodDto>(), Secrets: null),
|
|
},
|
|
DatabaseConnections: Array.Empty<DatabaseConnectionDto>(),
|
|
NotificationLists: Array.Empty<NotificationListDto>(),
|
|
SmtpConfigs: Array.Empty<SmtpConfigDto>(),
|
|
ApiKeys: Array.Empty<ApiKeyDto>(),
|
|
ApiMethods: Array.Empty<ApiMethodDto>());
|
|
|
|
var aggregate = new EntitySerializer().FromBundleContent(dto);
|
|
|
|
var sys = Assert.Single(aggregate.ExternalSystems);
|
|
Assert.Null(sys.AuthConfiguration);
|
|
}
|
|
}
|