diff --git a/src/ScadaLink.Commons/Class1.cs b/src/ScadaLink.Commons/Class1.cs deleted file mode 100644 index 68ec3ce..0000000 --- a/src/ScadaLink.Commons/Class1.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace ScadaLink.Commons; - -public class Class1 -{ - -} diff --git a/src/ScadaLink.Commons/Entities/Audit/AuditLogEntry.cs b/src/ScadaLink.Commons/Entities/Audit/AuditLogEntry.cs new file mode 100644 index 0000000..5f0e344 --- /dev/null +++ b/src/ScadaLink.Commons/Entities/Audit/AuditLogEntry.cs @@ -0,0 +1,22 @@ +namespace ScadaLink.Commons.Entities.Audit; + +public class AuditLogEntry +{ + public int Id { get; set; } + public string User { get; set; } + public string Action { get; set; } + public string EntityType { get; set; } + public string EntityId { get; set; } + public string EntityName { get; set; } + public string? AfterStateJson { get; set; } + public DateTimeOffset Timestamp { get; set; } + + public AuditLogEntry(string user, string action, string entityType, string entityId, string entityName) + { + User = user ?? throw new ArgumentNullException(nameof(user)); + Action = action ?? throw new ArgumentNullException(nameof(action)); + EntityType = entityType ?? throw new ArgumentNullException(nameof(entityType)); + EntityId = entityId ?? throw new ArgumentNullException(nameof(entityId)); + EntityName = entityName ?? throw new ArgumentNullException(nameof(entityName)); + } +} diff --git a/src/ScadaLink.Commons/Entities/Deployment/DeploymentRecord.cs b/src/ScadaLink.Commons/Entities/Deployment/DeploymentRecord.cs new file mode 100644 index 0000000..d0c95ed --- /dev/null +++ b/src/ScadaLink.Commons/Entities/Deployment/DeploymentRecord.cs @@ -0,0 +1,21 @@ +using ScadaLink.Commons.Types.Enums; + +namespace ScadaLink.Commons.Entities.Deployment; + +public class DeploymentRecord +{ + public int Id { get; set; } + public int InstanceId { get; set; } + public DeploymentStatus Status { get; set; } + public string DeploymentId { get; set; } + public string? RevisionHash { get; set; } + public string DeployedBy { get; set; } + public DateTimeOffset DeployedAt { get; set; } + public DateTimeOffset? CompletedAt { get; set; } + + public DeploymentRecord(string deploymentId, string deployedBy) + { + DeploymentId = deploymentId ?? throw new ArgumentNullException(nameof(deploymentId)); + DeployedBy = deployedBy ?? throw new ArgumentNullException(nameof(deployedBy)); + } +} diff --git a/src/ScadaLink.Commons/Entities/Deployment/SystemArtifactDeploymentRecord.cs b/src/ScadaLink.Commons/Entities/Deployment/SystemArtifactDeploymentRecord.cs new file mode 100644 index 0000000..c73fdf1 --- /dev/null +++ b/src/ScadaLink.Commons/Entities/Deployment/SystemArtifactDeploymentRecord.cs @@ -0,0 +1,16 @@ +namespace ScadaLink.Commons.Entities.Deployment; + +public class SystemArtifactDeploymentRecord +{ + public int Id { get; set; } + public string ArtifactType { get; set; } + public string DeployedBy { get; set; } + public DateTimeOffset DeployedAt { get; set; } + public string? PerSiteStatus { get; set; } + + public SystemArtifactDeploymentRecord(string artifactType, string deployedBy) + { + ArtifactType = artifactType ?? throw new ArgumentNullException(nameof(artifactType)); + DeployedBy = deployedBy ?? throw new ArgumentNullException(nameof(deployedBy)); + } +} diff --git a/src/ScadaLink.Commons/Entities/ExternalSystems/DatabaseConnectionDefinition.cs b/src/ScadaLink.Commons/Entities/ExternalSystems/DatabaseConnectionDefinition.cs new file mode 100644 index 0000000..c6f58de --- /dev/null +++ b/src/ScadaLink.Commons/Entities/ExternalSystems/DatabaseConnectionDefinition.cs @@ -0,0 +1,16 @@ +namespace ScadaLink.Commons.Entities.ExternalSystems; + +public class DatabaseConnectionDefinition +{ + public int Id { get; set; } + public string Name { get; set; } + public string ConnectionString { get; set; } + public int MaxRetries { get; set; } + public TimeSpan RetryDelay { get; set; } + + public DatabaseConnectionDefinition(string name, string connectionString) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + ConnectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString)); + } +} diff --git a/src/ScadaLink.Commons/Entities/ExternalSystems/ExternalSystemDefinition.cs b/src/ScadaLink.Commons/Entities/ExternalSystems/ExternalSystemDefinition.cs new file mode 100644 index 0000000..49c46b5 --- /dev/null +++ b/src/ScadaLink.Commons/Entities/ExternalSystems/ExternalSystemDefinition.cs @@ -0,0 +1,19 @@ +namespace ScadaLink.Commons.Entities.ExternalSystems; + +public class ExternalSystemDefinition +{ + public int Id { get; set; } + public string Name { get; set; } + public string EndpointUrl { get; set; } + public string AuthType { get; set; } + public string? AuthConfiguration { get; set; } + public int MaxRetries { get; set; } + public TimeSpan RetryDelay { get; set; } + + public ExternalSystemDefinition(string name, string endpointUrl, string authType) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + EndpointUrl = endpointUrl ?? throw new ArgumentNullException(nameof(endpointUrl)); + AuthType = authType ?? throw new ArgumentNullException(nameof(authType)); + } +} diff --git a/src/ScadaLink.Commons/Entities/ExternalSystems/ExternalSystemMethod.cs b/src/ScadaLink.Commons/Entities/ExternalSystems/ExternalSystemMethod.cs new file mode 100644 index 0000000..fb84a82 --- /dev/null +++ b/src/ScadaLink.Commons/Entities/ExternalSystems/ExternalSystemMethod.cs @@ -0,0 +1,19 @@ +namespace ScadaLink.Commons.Entities.ExternalSystems; + +public class ExternalSystemMethod +{ + public int Id { get; set; } + public int ExternalSystemDefinitionId { get; set; } + public string Name { get; set; } + public string HttpMethod { get; set; } + public string Path { get; set; } + public string? ParameterDefinitions { get; set; } + public string? ReturnDefinition { get; set; } + + public ExternalSystemMethod(string name, string httpMethod, string path) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + HttpMethod = httpMethod ?? throw new ArgumentNullException(nameof(httpMethod)); + Path = path ?? throw new ArgumentNullException(nameof(path)); + } +} diff --git a/src/ScadaLink.Commons/Entities/InboundApi/ApiKey.cs b/src/ScadaLink.Commons/Entities/InboundApi/ApiKey.cs new file mode 100644 index 0000000..7d5fd7c --- /dev/null +++ b/src/ScadaLink.Commons/Entities/InboundApi/ApiKey.cs @@ -0,0 +1,15 @@ +namespace ScadaLink.Commons.Entities.InboundApi; + +public class ApiKey +{ + public int Id { get; set; } + public string Name { get; set; } + public string KeyValue { get; set; } + public bool IsEnabled { get; set; } + + public ApiKey(string name, string keyValue) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + KeyValue = keyValue ?? throw new ArgumentNullException(nameof(keyValue)); + } +} diff --git a/src/ScadaLink.Commons/Entities/InboundApi/ApiMethod.cs b/src/ScadaLink.Commons/Entities/InboundApi/ApiMethod.cs new file mode 100644 index 0000000..9058b67 --- /dev/null +++ b/src/ScadaLink.Commons/Entities/InboundApi/ApiMethod.cs @@ -0,0 +1,18 @@ +namespace ScadaLink.Commons.Entities.InboundApi; + +public class ApiMethod +{ + public int Id { get; set; } + public string Name { get; set; } + public string Script { get; set; } + public string? ApprovedApiKeyIds { get; set; } + public string? ParameterDefinitions { get; set; } + public string? ReturnDefinition { get; set; } + public int TimeoutSeconds { get; set; } + + public ApiMethod(string name, string script) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + Script = script ?? throw new ArgumentNullException(nameof(script)); + } +} diff --git a/src/ScadaLink.Commons/Entities/Instances/Area.cs b/src/ScadaLink.Commons/Entities/Instances/Area.cs new file mode 100644 index 0000000..bcd14e8 --- /dev/null +++ b/src/ScadaLink.Commons/Entities/Instances/Area.cs @@ -0,0 +1,15 @@ +namespace ScadaLink.Commons.Entities.Instances; + +public class Area +{ + public int Id { get; set; } + public int SiteId { get; set; } + public string Name { get; set; } + public int? ParentAreaId { get; set; } + public ICollection Children { get; set; } = new List(); + + public Area(string name) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + } +} diff --git a/src/ScadaLink.Commons/Entities/Instances/Instance.cs b/src/ScadaLink.Commons/Entities/Instances/Instance.cs new file mode 100644 index 0000000..e1c2ca6 --- /dev/null +++ b/src/ScadaLink.Commons/Entities/Instances/Instance.cs @@ -0,0 +1,20 @@ +using ScadaLink.Commons.Types.Enums; + +namespace ScadaLink.Commons.Entities.Instances; + +public class Instance +{ + public int Id { get; set; } + public int TemplateId { get; set; } + public int SiteId { get; set; } + public int? AreaId { get; set; } + public string UniqueName { get; set; } + public InstanceState State { get; set; } + public ICollection AttributeOverrides { get; set; } = new List(); + public ICollection ConnectionBindings { get; set; } = new List(); + + public Instance(string uniqueName) + { + UniqueName = uniqueName ?? throw new ArgumentNullException(nameof(uniqueName)); + } +} diff --git a/src/ScadaLink.Commons/Entities/Instances/InstanceAttributeOverride.cs b/src/ScadaLink.Commons/Entities/Instances/InstanceAttributeOverride.cs new file mode 100644 index 0000000..bd59428 --- /dev/null +++ b/src/ScadaLink.Commons/Entities/Instances/InstanceAttributeOverride.cs @@ -0,0 +1,14 @@ +namespace ScadaLink.Commons.Entities.Instances; + +public class InstanceAttributeOverride +{ + public int Id { get; set; } + public int InstanceId { get; set; } + public string AttributeName { get; set; } + public string? OverrideValue { get; set; } + + public InstanceAttributeOverride(string attributeName) + { + AttributeName = attributeName ?? throw new ArgumentNullException(nameof(attributeName)); + } +} diff --git a/src/ScadaLink.Commons/Entities/Instances/InstanceConnectionBinding.cs b/src/ScadaLink.Commons/Entities/Instances/InstanceConnectionBinding.cs new file mode 100644 index 0000000..6dc027b --- /dev/null +++ b/src/ScadaLink.Commons/Entities/Instances/InstanceConnectionBinding.cs @@ -0,0 +1,14 @@ +namespace ScadaLink.Commons.Entities.Instances; + +public class InstanceConnectionBinding +{ + public int Id { get; set; } + public int InstanceId { get; set; } + public string AttributeName { get; set; } + public int DataConnectionId { get; set; } + + public InstanceConnectionBinding(string attributeName) + { + AttributeName = attributeName ?? throw new ArgumentNullException(nameof(attributeName)); + } +} diff --git a/src/ScadaLink.Commons/Entities/Notifications/NotificationList.cs b/src/ScadaLink.Commons/Entities/Notifications/NotificationList.cs new file mode 100644 index 0000000..1c78946 --- /dev/null +++ b/src/ScadaLink.Commons/Entities/Notifications/NotificationList.cs @@ -0,0 +1,13 @@ +namespace ScadaLink.Commons.Entities.Notifications; + +public class NotificationList +{ + public int Id { get; set; } + public string Name { get; set; } + public ICollection Recipients { get; set; } = new List(); + + public NotificationList(string name) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + } +} diff --git a/src/ScadaLink.Commons/Entities/Notifications/NotificationRecipient.cs b/src/ScadaLink.Commons/Entities/Notifications/NotificationRecipient.cs new file mode 100644 index 0000000..706d047 --- /dev/null +++ b/src/ScadaLink.Commons/Entities/Notifications/NotificationRecipient.cs @@ -0,0 +1,15 @@ +namespace ScadaLink.Commons.Entities.Notifications; + +public class NotificationRecipient +{ + public int Id { get; set; } + public int NotificationListId { get; set; } + public string Name { get; set; } + public string EmailAddress { get; set; } + + public NotificationRecipient(string name, string emailAddress) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + EmailAddress = emailAddress ?? throw new ArgumentNullException(nameof(emailAddress)); + } +} diff --git a/src/ScadaLink.Commons/Entities/Notifications/SmtpConfiguration.cs b/src/ScadaLink.Commons/Entities/Notifications/SmtpConfiguration.cs new file mode 100644 index 0000000..0481ef8 --- /dev/null +++ b/src/ScadaLink.Commons/Entities/Notifications/SmtpConfiguration.cs @@ -0,0 +1,23 @@ +namespace ScadaLink.Commons.Entities.Notifications; + +public class SmtpConfiguration +{ + public int Id { get; set; } + public string Host { get; set; } + public int Port { get; set; } + public string AuthType { get; set; } + public string? Credentials { get; set; } + public string? TlsMode { get; set; } + public string FromAddress { get; set; } + public int ConnectionTimeoutSeconds { get; set; } + public int MaxConcurrentConnections { get; set; } + public int MaxRetries { get; set; } + public TimeSpan RetryDelay { get; set; } + + public SmtpConfiguration(string host, string authType, string fromAddress) + { + Host = host ?? throw new ArgumentNullException(nameof(host)); + AuthType = authType ?? throw new ArgumentNullException(nameof(authType)); + FromAddress = fromAddress ?? throw new ArgumentNullException(nameof(fromAddress)); + } +} diff --git a/src/ScadaLink.Commons/Entities/Scripts/SharedScript.cs b/src/ScadaLink.Commons/Entities/Scripts/SharedScript.cs new file mode 100644 index 0000000..ede07e5 --- /dev/null +++ b/src/ScadaLink.Commons/Entities/Scripts/SharedScript.cs @@ -0,0 +1,16 @@ +namespace ScadaLink.Commons.Entities.Scripts; + +public class SharedScript +{ + public int Id { get; set; } + public string Name { get; set; } + public string Code { get; set; } + public string? ParameterDefinitions { get; set; } + public string? ReturnDefinition { get; set; } + + public SharedScript(string name, string code) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + Code = code ?? throw new ArgumentNullException(nameof(code)); + } +} diff --git a/src/ScadaLink.Commons/Entities/Security/LdapGroupMapping.cs b/src/ScadaLink.Commons/Entities/Security/LdapGroupMapping.cs new file mode 100644 index 0000000..c6c5827 --- /dev/null +++ b/src/ScadaLink.Commons/Entities/Security/LdapGroupMapping.cs @@ -0,0 +1,14 @@ +namespace ScadaLink.Commons.Entities.Security; + +public class LdapGroupMapping +{ + public int Id { get; set; } + public string LdapGroupName { get; set; } + public string Role { get; set; } + + public LdapGroupMapping(string ldapGroupName, string role) + { + LdapGroupName = ldapGroupName ?? throw new ArgumentNullException(nameof(ldapGroupName)); + Role = role ?? throw new ArgumentNullException(nameof(role)); + } +} diff --git a/src/ScadaLink.Commons/Entities/Security/SiteScopeRule.cs b/src/ScadaLink.Commons/Entities/Security/SiteScopeRule.cs new file mode 100644 index 0000000..06e92b9 --- /dev/null +++ b/src/ScadaLink.Commons/Entities/Security/SiteScopeRule.cs @@ -0,0 +1,8 @@ +namespace ScadaLink.Commons.Entities.Security; + +public class SiteScopeRule +{ + public int Id { get; set; } + public int LdapGroupMappingId { get; set; } + public int SiteId { get; set; } +} diff --git a/src/ScadaLink.Commons/Entities/Sites/DataConnection.cs b/src/ScadaLink.Commons/Entities/Sites/DataConnection.cs new file mode 100644 index 0000000..397cf38 --- /dev/null +++ b/src/ScadaLink.Commons/Entities/Sites/DataConnection.cs @@ -0,0 +1,15 @@ +namespace ScadaLink.Commons.Entities.Sites; + +public class DataConnection +{ + public int Id { get; set; } + public string Name { get; set; } + public string Protocol { get; set; } + public string? Configuration { get; set; } + + public DataConnection(string name, string protocol) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + Protocol = protocol ?? throw new ArgumentNullException(nameof(protocol)); + } +} diff --git a/src/ScadaLink.Commons/Entities/Sites/Site.cs b/src/ScadaLink.Commons/Entities/Sites/Site.cs new file mode 100644 index 0000000..d953e90 --- /dev/null +++ b/src/ScadaLink.Commons/Entities/Sites/Site.cs @@ -0,0 +1,15 @@ +namespace ScadaLink.Commons.Entities.Sites; + +public class Site +{ + public int Id { get; set; } + public string Name { get; set; } + public string SiteIdentifier { get; set; } + public string? Description { get; set; } + + public Site(string name, string siteIdentifier) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + SiteIdentifier = siteIdentifier ?? throw new ArgumentNullException(nameof(siteIdentifier)); + } +} diff --git a/src/ScadaLink.Commons/Entities/Sites/SiteDataConnectionAssignment.cs b/src/ScadaLink.Commons/Entities/Sites/SiteDataConnectionAssignment.cs new file mode 100644 index 0000000..2caca44 --- /dev/null +++ b/src/ScadaLink.Commons/Entities/Sites/SiteDataConnectionAssignment.cs @@ -0,0 +1,8 @@ +namespace ScadaLink.Commons.Entities.Sites; + +public class SiteDataConnectionAssignment +{ + public int Id { get; set; } + public int SiteId { get; set; } + public int DataConnectionId { get; set; } +} diff --git a/src/ScadaLink.Commons/Entities/Templates/Template.cs b/src/ScadaLink.Commons/Entities/Templates/Template.cs new file mode 100644 index 0000000..35633ac --- /dev/null +++ b/src/ScadaLink.Commons/Entities/Templates/Template.cs @@ -0,0 +1,18 @@ +namespace ScadaLink.Commons.Entities.Templates; + +public class Template +{ + public int Id { get; set; } + public string Name { get; set; } + public string? Description { get; set; } + public int? ParentTemplateId { get; set; } + public ICollection Attributes { get; set; } = new List(); + public ICollection Alarms { get; set; } = new List(); + public ICollection Scripts { get; set; } = new List(); + public ICollection Compositions { get; set; } = new List(); + + public Template(string name) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + } +} diff --git a/src/ScadaLink.Commons/Entities/Templates/TemplateAlarm.cs b/src/ScadaLink.Commons/Entities/Templates/TemplateAlarm.cs new file mode 100644 index 0000000..fe79fb9 --- /dev/null +++ b/src/ScadaLink.Commons/Entities/Templates/TemplateAlarm.cs @@ -0,0 +1,21 @@ +using ScadaLink.Commons.Types.Enums; + +namespace ScadaLink.Commons.Entities.Templates; + +public class TemplateAlarm +{ + public int Id { get; set; } + public int TemplateId { get; set; } + public string Name { get; set; } + public string? Description { get; set; } + public int PriorityLevel { get; set; } + public bool IsLocked { get; set; } + public AlarmTriggerType TriggerType { get; set; } + public string? TriggerConfiguration { get; set; } + public int? OnTriggerScriptId { get; set; } + + public TemplateAlarm(string name) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + } +} diff --git a/src/ScadaLink.Commons/Entities/Templates/TemplateAttribute.cs b/src/ScadaLink.Commons/Entities/Templates/TemplateAttribute.cs new file mode 100644 index 0000000..f3fb9ef --- /dev/null +++ b/src/ScadaLink.Commons/Entities/Templates/TemplateAttribute.cs @@ -0,0 +1,20 @@ +using ScadaLink.Commons.Types.Enums; + +namespace ScadaLink.Commons.Entities.Templates; + +public class TemplateAttribute +{ + public int Id { get; set; } + public int TemplateId { get; set; } + public string Name { get; set; } + public string? Value { get; set; } + public DataType DataType { get; set; } + public bool IsLocked { get; set; } + public string? Description { get; set; } + public string? DataSourceReference { get; set; } + + public TemplateAttribute(string name) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + } +} diff --git a/src/ScadaLink.Commons/Entities/Templates/TemplateComposition.cs b/src/ScadaLink.Commons/Entities/Templates/TemplateComposition.cs new file mode 100644 index 0000000..2c6e536 --- /dev/null +++ b/src/ScadaLink.Commons/Entities/Templates/TemplateComposition.cs @@ -0,0 +1,14 @@ +namespace ScadaLink.Commons.Entities.Templates; + +public class TemplateComposition +{ + public int Id { get; set; } + public int TemplateId { get; set; } + public int ComposedTemplateId { get; set; } + public string InstanceName { get; set; } + + public TemplateComposition(string instanceName) + { + InstanceName = instanceName ?? throw new ArgumentNullException(nameof(instanceName)); + } +} diff --git a/src/ScadaLink.Commons/Entities/Templates/TemplateScript.cs b/src/ScadaLink.Commons/Entities/Templates/TemplateScript.cs new file mode 100644 index 0000000..896e2fa --- /dev/null +++ b/src/ScadaLink.Commons/Entities/Templates/TemplateScript.cs @@ -0,0 +1,21 @@ +namespace ScadaLink.Commons.Entities.Templates; + +public class TemplateScript +{ + public int Id { get; set; } + public int TemplateId { get; set; } + public string Name { get; set; } + public bool IsLocked { get; set; } + public string Code { get; set; } + public string? TriggerType { get; set; } + public string? TriggerConfiguration { get; set; } + public string? ParameterDefinitions { get; set; } + public string? ReturnDefinition { get; set; } + public TimeSpan? MinTimeBetweenRuns { get; set; } + + public TemplateScript(string name, string code) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + Code = code ?? throw new ArgumentNullException(nameof(code)); + } +} diff --git a/src/ScadaLink.Commons/Interfaces/Protocol/IDataConnection.cs b/src/ScadaLink.Commons/Interfaces/Protocol/IDataConnection.cs new file mode 100644 index 0000000..7d959b6 --- /dev/null +++ b/src/ScadaLink.Commons/Interfaces/Protocol/IDataConnection.cs @@ -0,0 +1,25 @@ +using ScadaLink.Commons.Types.Enums; + +namespace ScadaLink.Commons.Interfaces.Protocol; + +public enum QualityCode { Good, Bad, Uncertain } + +public record TagValue(object? Value, QualityCode Quality, DateTimeOffset Timestamp); +public record ReadResult(bool Success, TagValue? Value, string? ErrorMessage); +public record WriteResult(bool Success, string? ErrorMessage); + +public delegate void SubscriptionCallback(string tagPath, TagValue value); + +public interface IDataConnection : IAsyncDisposable +{ + Task ConnectAsync(IDictionary connectionDetails, CancellationToken cancellationToken = default); + Task DisconnectAsync(CancellationToken cancellationToken = default); + Task SubscribeAsync(string tagPath, SubscriptionCallback callback, CancellationToken cancellationToken = default); + Task UnsubscribeAsync(string subscriptionId, CancellationToken cancellationToken = default); + Task ReadAsync(string tagPath, CancellationToken cancellationToken = default); + Task> ReadBatchAsync(IEnumerable tagPaths, CancellationToken cancellationToken = default); + Task WriteAsync(string tagPath, object? value, CancellationToken cancellationToken = default); + Task> WriteBatchAsync(IDictionary values, CancellationToken cancellationToken = default); + Task WriteBatchAndWaitAsync(IDictionary values, string flagPath, object? flagValue, string responsePath, object? responseValue, TimeSpan timeout, CancellationToken cancellationToken = default); + ConnectionHealth Status { get; } +} diff --git a/src/ScadaLink.Commons/Interfaces/Repositories/ICentralUiRepository.cs b/src/ScadaLink.Commons/Interfaces/Repositories/ICentralUiRepository.cs new file mode 100644 index 0000000..753c3aa --- /dev/null +++ b/src/ScadaLink.Commons/Interfaces/Repositories/ICentralUiRepository.cs @@ -0,0 +1,19 @@ +using ScadaLink.Commons.Entities.Deployment; +using ScadaLink.Commons.Entities.Instances; +using ScadaLink.Commons.Entities.Sites; +using ScadaLink.Commons.Entities.Templates; + +namespace ScadaLink.Commons.Interfaces.Repositories; + +public interface ICentralUiRepository +{ + Task> GetAllSitesAsync(CancellationToken cancellationToken = default); + Task> GetDataConnectionsBySiteIdAsync(int siteId, CancellationToken cancellationToken = default); + Task> GetAllSiteDataConnectionAssignmentsAsync(CancellationToken cancellationToken = default); + Task> GetTemplateTreeAsync(CancellationToken cancellationToken = default); + Task> GetInstancesFilteredAsync(int? siteId = null, int? templateId = null, string? searchTerm = null, CancellationToken cancellationToken = default); + Task> GetRecentDeploymentsAsync(int count, CancellationToken cancellationToken = default); + Task> GetAreaTreeBySiteIdAsync(int siteId, CancellationToken cancellationToken = default); + + Task SaveChangesAsync(CancellationToken cancellationToken = default); +} diff --git a/src/ScadaLink.Commons/Interfaces/Repositories/IDeploymentManagerRepository.cs b/src/ScadaLink.Commons/Interfaces/Repositories/IDeploymentManagerRepository.cs new file mode 100644 index 0000000..ebe597a --- /dev/null +++ b/src/ScadaLink.Commons/Interfaces/Repositories/IDeploymentManagerRepository.cs @@ -0,0 +1,26 @@ +using ScadaLink.Commons.Entities.Deployment; +using ScadaLink.Commons.Types.Enums; + +namespace ScadaLink.Commons.Interfaces.Repositories; + +public interface IDeploymentManagerRepository +{ + // DeploymentRecord + Task GetDeploymentRecordByIdAsync(int id, CancellationToken cancellationToken = default); + Task> GetAllDeploymentRecordsAsync(CancellationToken cancellationToken = default); + Task> GetDeploymentsByInstanceIdAsync(int instanceId, CancellationToken cancellationToken = default); + Task GetCurrentDeploymentStatusAsync(int instanceId, CancellationToken cancellationToken = default); + Task GetDeploymentByDeploymentIdAsync(string deploymentId, CancellationToken cancellationToken = default); + Task AddDeploymentRecordAsync(DeploymentRecord record, CancellationToken cancellationToken = default); + Task UpdateDeploymentRecordAsync(DeploymentRecord record, CancellationToken cancellationToken = default); + Task DeleteDeploymentRecordAsync(int id, CancellationToken cancellationToken = default); + + // SystemArtifactDeploymentRecord + Task GetSystemArtifactDeploymentByIdAsync(int id, CancellationToken cancellationToken = default); + Task> GetAllSystemArtifactDeploymentsAsync(CancellationToken cancellationToken = default); + Task AddSystemArtifactDeploymentAsync(SystemArtifactDeploymentRecord record, CancellationToken cancellationToken = default); + Task UpdateSystemArtifactDeploymentAsync(SystemArtifactDeploymentRecord record, CancellationToken cancellationToken = default); + Task DeleteSystemArtifactDeploymentAsync(int id, CancellationToken cancellationToken = default); + + Task SaveChangesAsync(CancellationToken cancellationToken = default); +} diff --git a/src/ScadaLink.Commons/Interfaces/Repositories/IExternalSystemRepository.cs b/src/ScadaLink.Commons/Interfaces/Repositories/IExternalSystemRepository.cs new file mode 100644 index 0000000..0410030 --- /dev/null +++ b/src/ScadaLink.Commons/Interfaces/Repositories/IExternalSystemRepository.cs @@ -0,0 +1,29 @@ +using ScadaLink.Commons.Entities.ExternalSystems; + +namespace ScadaLink.Commons.Interfaces.Repositories; + +public interface IExternalSystemRepository +{ + // ExternalSystemDefinition + Task GetExternalSystemByIdAsync(int id, CancellationToken cancellationToken = default); + Task> GetAllExternalSystemsAsync(CancellationToken cancellationToken = default); + Task AddExternalSystemAsync(ExternalSystemDefinition definition, CancellationToken cancellationToken = default); + Task UpdateExternalSystemAsync(ExternalSystemDefinition definition, CancellationToken cancellationToken = default); + Task DeleteExternalSystemAsync(int id, CancellationToken cancellationToken = default); + + // ExternalSystemMethod + Task GetExternalSystemMethodByIdAsync(int id, CancellationToken cancellationToken = default); + Task> GetMethodsByExternalSystemIdAsync(int externalSystemId, CancellationToken cancellationToken = default); + Task AddExternalSystemMethodAsync(ExternalSystemMethod method, CancellationToken cancellationToken = default); + Task UpdateExternalSystemMethodAsync(ExternalSystemMethod method, CancellationToken cancellationToken = default); + Task DeleteExternalSystemMethodAsync(int id, CancellationToken cancellationToken = default); + + // DatabaseConnectionDefinition + Task GetDatabaseConnectionByIdAsync(int id, CancellationToken cancellationToken = default); + Task> GetAllDatabaseConnectionsAsync(CancellationToken cancellationToken = default); + Task AddDatabaseConnectionAsync(DatabaseConnectionDefinition definition, CancellationToken cancellationToken = default); + Task UpdateDatabaseConnectionAsync(DatabaseConnectionDefinition definition, CancellationToken cancellationToken = default); + Task DeleteDatabaseConnectionAsync(int id, CancellationToken cancellationToken = default); + + Task SaveChangesAsync(CancellationToken cancellationToken = default); +} diff --git a/src/ScadaLink.Commons/Interfaces/Repositories/IInboundApiRepository.cs b/src/ScadaLink.Commons/Interfaces/Repositories/IInboundApiRepository.cs new file mode 100644 index 0000000..bef4ef6 --- /dev/null +++ b/src/ScadaLink.Commons/Interfaces/Repositories/IInboundApiRepository.cs @@ -0,0 +1,25 @@ +using ScadaLink.Commons.Entities.InboundApi; + +namespace ScadaLink.Commons.Interfaces.Repositories; + +public interface IInboundApiRepository +{ + // ApiKey + Task GetApiKeyByIdAsync(int id, CancellationToken cancellationToken = default); + Task> GetAllApiKeysAsync(CancellationToken cancellationToken = default); + Task GetApiKeyByValueAsync(string keyValue, CancellationToken cancellationToken = default); + Task AddApiKeyAsync(ApiKey apiKey, CancellationToken cancellationToken = default); + Task UpdateApiKeyAsync(ApiKey apiKey, CancellationToken cancellationToken = default); + Task DeleteApiKeyAsync(int id, CancellationToken cancellationToken = default); + + // ApiMethod + Task GetApiMethodByIdAsync(int id, CancellationToken cancellationToken = default); + Task> GetAllApiMethodsAsync(CancellationToken cancellationToken = default); + Task GetMethodByNameAsync(string name, CancellationToken cancellationToken = default); + Task> GetApprovedKeysForMethodAsync(int methodId, CancellationToken cancellationToken = default); + Task AddApiMethodAsync(ApiMethod method, CancellationToken cancellationToken = default); + Task UpdateApiMethodAsync(ApiMethod method, CancellationToken cancellationToken = default); + Task DeleteApiMethodAsync(int id, CancellationToken cancellationToken = default); + + Task SaveChangesAsync(CancellationToken cancellationToken = default); +} diff --git a/src/ScadaLink.Commons/Interfaces/Repositories/INotificationRepository.cs b/src/ScadaLink.Commons/Interfaces/Repositories/INotificationRepository.cs new file mode 100644 index 0000000..512e85d --- /dev/null +++ b/src/ScadaLink.Commons/Interfaces/Repositories/INotificationRepository.cs @@ -0,0 +1,30 @@ +using ScadaLink.Commons.Entities.Notifications; + +namespace ScadaLink.Commons.Interfaces.Repositories; + +public interface INotificationRepository +{ + // NotificationList + Task GetNotificationListByIdAsync(int id, CancellationToken cancellationToken = default); + Task> GetAllNotificationListsAsync(CancellationToken cancellationToken = default); + Task GetListByNameAsync(string name, CancellationToken cancellationToken = default); + Task AddNotificationListAsync(NotificationList list, CancellationToken cancellationToken = default); + Task UpdateNotificationListAsync(NotificationList list, CancellationToken cancellationToken = default); + Task DeleteNotificationListAsync(int id, CancellationToken cancellationToken = default); + + // NotificationRecipient + Task GetRecipientByIdAsync(int id, CancellationToken cancellationToken = default); + Task> GetRecipientsByListIdAsync(int notificationListId, CancellationToken cancellationToken = default); + Task AddRecipientAsync(NotificationRecipient recipient, CancellationToken cancellationToken = default); + Task UpdateRecipientAsync(NotificationRecipient recipient, CancellationToken cancellationToken = default); + Task DeleteRecipientAsync(int id, CancellationToken cancellationToken = default); + + // SmtpConfiguration + Task GetSmtpConfigurationByIdAsync(int id, CancellationToken cancellationToken = default); + Task> GetAllSmtpConfigurationsAsync(CancellationToken cancellationToken = default); + Task AddSmtpConfigurationAsync(SmtpConfiguration configuration, CancellationToken cancellationToken = default); + Task UpdateSmtpConfigurationAsync(SmtpConfiguration configuration, CancellationToken cancellationToken = default); + Task DeleteSmtpConfigurationAsync(int id, CancellationToken cancellationToken = default); + + Task SaveChangesAsync(CancellationToken cancellationToken = default); +} diff --git a/src/ScadaLink.Commons/Interfaces/Repositories/ISecurityRepository.cs b/src/ScadaLink.Commons/Interfaces/Repositories/ISecurityRepository.cs new file mode 100644 index 0000000..c9e7ccb --- /dev/null +++ b/src/ScadaLink.Commons/Interfaces/Repositories/ISecurityRepository.cs @@ -0,0 +1,23 @@ +using ScadaLink.Commons.Entities.Security; + +namespace ScadaLink.Commons.Interfaces.Repositories; + +public interface ISecurityRepository +{ + // LdapGroupMapping + Task GetMappingByIdAsync(int id, CancellationToken cancellationToken = default); + Task> GetAllMappingsAsync(CancellationToken cancellationToken = default); + Task> GetMappingsByRoleAsync(string role, CancellationToken cancellationToken = default); + Task AddMappingAsync(LdapGroupMapping mapping, CancellationToken cancellationToken = default); + Task UpdateMappingAsync(LdapGroupMapping mapping, CancellationToken cancellationToken = default); + Task DeleteMappingAsync(int id, CancellationToken cancellationToken = default); + + // SiteScopeRule + Task GetScopeRuleByIdAsync(int id, CancellationToken cancellationToken = default); + Task> GetScopeRulesForMappingAsync(int ldapGroupMappingId, CancellationToken cancellationToken = default); + Task AddScopeRuleAsync(SiteScopeRule rule, CancellationToken cancellationToken = default); + Task UpdateScopeRuleAsync(SiteScopeRule rule, CancellationToken cancellationToken = default); + Task DeleteScopeRuleAsync(int id, CancellationToken cancellationToken = default); + + Task SaveChangesAsync(CancellationToken cancellationToken = default); +} diff --git a/src/ScadaLink.Commons/Interfaces/Repositories/ITemplateEngineRepository.cs b/src/ScadaLink.Commons/Interfaces/Repositories/ITemplateEngineRepository.cs new file mode 100644 index 0000000..ceedde9 --- /dev/null +++ b/src/ScadaLink.Commons/Interfaces/Repositories/ITemplateEngineRepository.cs @@ -0,0 +1,74 @@ +using ScadaLink.Commons.Entities.Instances; +using ScadaLink.Commons.Entities.Templates; + +namespace ScadaLink.Commons.Interfaces.Repositories; + +public interface ITemplateEngineRepository +{ + // Template + Task GetTemplateByIdAsync(int id, CancellationToken cancellationToken = default); + Task GetTemplateWithChildrenAsync(int id, CancellationToken cancellationToken = default); + Task> GetAllTemplatesAsync(CancellationToken cancellationToken = default); + Task AddTemplateAsync(Template template, CancellationToken cancellationToken = default); + Task UpdateTemplateAsync(Template template, CancellationToken cancellationToken = default); + Task DeleteTemplateAsync(int id, CancellationToken cancellationToken = default); + + // TemplateAttribute + Task GetTemplateAttributeByIdAsync(int id, CancellationToken cancellationToken = default); + Task> GetAttributesByTemplateIdAsync(int templateId, CancellationToken cancellationToken = default); + Task AddTemplateAttributeAsync(TemplateAttribute attribute, CancellationToken cancellationToken = default); + Task UpdateTemplateAttributeAsync(TemplateAttribute attribute, CancellationToken cancellationToken = default); + Task DeleteTemplateAttributeAsync(int id, CancellationToken cancellationToken = default); + + // TemplateAlarm + Task GetTemplateAlarmByIdAsync(int id, CancellationToken cancellationToken = default); + Task> GetAlarmsByTemplateIdAsync(int templateId, CancellationToken cancellationToken = default); + Task AddTemplateAlarmAsync(TemplateAlarm alarm, CancellationToken cancellationToken = default); + Task UpdateTemplateAlarmAsync(TemplateAlarm alarm, CancellationToken cancellationToken = default); + Task DeleteTemplateAlarmAsync(int id, CancellationToken cancellationToken = default); + + // TemplateScript + Task GetTemplateScriptByIdAsync(int id, CancellationToken cancellationToken = default); + Task> GetScriptsByTemplateIdAsync(int templateId, CancellationToken cancellationToken = default); + Task AddTemplateScriptAsync(TemplateScript script, CancellationToken cancellationToken = default); + Task UpdateTemplateScriptAsync(TemplateScript script, CancellationToken cancellationToken = default); + Task DeleteTemplateScriptAsync(int id, CancellationToken cancellationToken = default); + + // TemplateComposition + Task GetTemplateCompositionByIdAsync(int id, CancellationToken cancellationToken = default); + Task> GetCompositionsByTemplateIdAsync(int templateId, CancellationToken cancellationToken = default); + Task AddTemplateCompositionAsync(TemplateComposition composition, CancellationToken cancellationToken = default); + Task UpdateTemplateCompositionAsync(TemplateComposition composition, CancellationToken cancellationToken = default); + Task DeleteTemplateCompositionAsync(int id, CancellationToken cancellationToken = default); + + // Instance + Task GetInstanceByIdAsync(int id, CancellationToken cancellationToken = default); + Task> GetAllInstancesAsync(CancellationToken cancellationToken = default); + Task> GetInstancesByTemplateIdAsync(int templateId, CancellationToken cancellationToken = default); + Task> GetInstancesBySiteIdAsync(int siteId, CancellationToken cancellationToken = default); + Task GetInstanceByUniqueNameAsync(string uniqueName, CancellationToken cancellationToken = default); + Task AddInstanceAsync(Instance instance, CancellationToken cancellationToken = default); + Task UpdateInstanceAsync(Instance instance, CancellationToken cancellationToken = default); + Task DeleteInstanceAsync(int id, CancellationToken cancellationToken = default); + + // InstanceAttributeOverride + Task> GetOverridesByInstanceIdAsync(int instanceId, CancellationToken cancellationToken = default); + Task AddInstanceAttributeOverrideAsync(InstanceAttributeOverride attributeOverride, CancellationToken cancellationToken = default); + Task UpdateInstanceAttributeOverrideAsync(InstanceAttributeOverride attributeOverride, CancellationToken cancellationToken = default); + Task DeleteInstanceAttributeOverrideAsync(int id, CancellationToken cancellationToken = default); + + // InstanceConnectionBinding + Task> GetBindingsByInstanceIdAsync(int instanceId, CancellationToken cancellationToken = default); + Task AddInstanceConnectionBindingAsync(InstanceConnectionBinding binding, CancellationToken cancellationToken = default); + Task UpdateInstanceConnectionBindingAsync(InstanceConnectionBinding binding, CancellationToken cancellationToken = default); + Task DeleteInstanceConnectionBindingAsync(int id, CancellationToken cancellationToken = default); + + // Area + Task GetAreaByIdAsync(int id, CancellationToken cancellationToken = default); + Task> GetAreasBySiteIdAsync(int siteId, CancellationToken cancellationToken = default); + Task AddAreaAsync(Area area, CancellationToken cancellationToken = default); + Task UpdateAreaAsync(Area area, CancellationToken cancellationToken = default); + Task DeleteAreaAsync(int id, CancellationToken cancellationToken = default); + + Task SaveChangesAsync(CancellationToken cancellationToken = default); +} diff --git a/src/ScadaLink.Commons/Interfaces/Services/IAuditService.cs b/src/ScadaLink.Commons/Interfaces/Services/IAuditService.cs new file mode 100644 index 0000000..984bc3f --- /dev/null +++ b/src/ScadaLink.Commons/Interfaces/Services/IAuditService.cs @@ -0,0 +1,6 @@ +namespace ScadaLink.Commons.Interfaces.Services; + +public interface IAuditService +{ + Task LogAsync(string user, string action, string entityType, string entityId, string entityName, object? afterState, CancellationToken cancellationToken = default); +} diff --git a/src/ScadaLink.Commons/Messages/Artifacts/ArtifactDeploymentResponse.cs b/src/ScadaLink.Commons/Messages/Artifacts/ArtifactDeploymentResponse.cs new file mode 100644 index 0000000..ab7c578 --- /dev/null +++ b/src/ScadaLink.Commons/Messages/Artifacts/ArtifactDeploymentResponse.cs @@ -0,0 +1,8 @@ +namespace ScadaLink.Commons.Messages.Artifacts; + +public record ArtifactDeploymentResponse( + string DeploymentId, + string SiteId, + bool Success, + string? ErrorMessage, + DateTimeOffset Timestamp); diff --git a/src/ScadaLink.Commons/Messages/Artifacts/DatabaseConnectionArtifact.cs b/src/ScadaLink.Commons/Messages/Artifacts/DatabaseConnectionArtifact.cs new file mode 100644 index 0000000..7d95d81 --- /dev/null +++ b/src/ScadaLink.Commons/Messages/Artifacts/DatabaseConnectionArtifact.cs @@ -0,0 +1,7 @@ +namespace ScadaLink.Commons.Messages.Artifacts; + +public record DatabaseConnectionArtifact( + string Name, + string ConnectionString, + int MaxRetries, + TimeSpan RetryDelay); diff --git a/src/ScadaLink.Commons/Messages/Artifacts/DeployArtifactsCommand.cs b/src/ScadaLink.Commons/Messages/Artifacts/DeployArtifactsCommand.cs new file mode 100644 index 0000000..db0db25 --- /dev/null +++ b/src/ScadaLink.Commons/Messages/Artifacts/DeployArtifactsCommand.cs @@ -0,0 +1,9 @@ +namespace ScadaLink.Commons.Messages.Artifacts; + +public record DeployArtifactsCommand( + string DeploymentId, + IReadOnlyList? SharedScripts, + IReadOnlyList? ExternalSystems, + IReadOnlyList? DatabaseConnections, + IReadOnlyList? NotificationLists, + DateTimeOffset Timestamp); diff --git a/src/ScadaLink.Commons/Messages/Artifacts/ExternalSystemArtifact.cs b/src/ScadaLink.Commons/Messages/Artifacts/ExternalSystemArtifact.cs new file mode 100644 index 0000000..f212ae8 --- /dev/null +++ b/src/ScadaLink.Commons/Messages/Artifacts/ExternalSystemArtifact.cs @@ -0,0 +1,8 @@ +namespace ScadaLink.Commons.Messages.Artifacts; + +public record ExternalSystemArtifact( + string Name, + string EndpointUrl, + string AuthType, + string? AuthConfiguration, + string? MethodDefinitionsJson); diff --git a/src/ScadaLink.Commons/Messages/Artifacts/NotificationListArtifact.cs b/src/ScadaLink.Commons/Messages/Artifacts/NotificationListArtifact.cs new file mode 100644 index 0000000..36a4d9a --- /dev/null +++ b/src/ScadaLink.Commons/Messages/Artifacts/NotificationListArtifact.cs @@ -0,0 +1,5 @@ +namespace ScadaLink.Commons.Messages.Artifacts; + +public record NotificationListArtifact( + string Name, + IReadOnlyList RecipientEmails); diff --git a/src/ScadaLink.Commons/Messages/Artifacts/SharedScriptArtifact.cs b/src/ScadaLink.Commons/Messages/Artifacts/SharedScriptArtifact.cs new file mode 100644 index 0000000..20e2e61 --- /dev/null +++ b/src/ScadaLink.Commons/Messages/Artifacts/SharedScriptArtifact.cs @@ -0,0 +1,7 @@ +namespace ScadaLink.Commons.Messages.Artifacts; + +public record SharedScriptArtifact( + string Name, + string Code, + string? ParameterDefinitions, + string? ReturnDefinition); diff --git a/src/ScadaLink.Commons/Messages/Communication/ConnectionStateChanged.cs b/src/ScadaLink.Commons/Messages/Communication/ConnectionStateChanged.cs new file mode 100644 index 0000000..ab06763 --- /dev/null +++ b/src/ScadaLink.Commons/Messages/Communication/ConnectionStateChanged.cs @@ -0,0 +1,6 @@ +namespace ScadaLink.Commons.Messages.Communication; + +public record ConnectionStateChanged( + string SiteId, + bool IsConnected, + DateTimeOffset Timestamp); diff --git a/src/ScadaLink.Commons/Messages/Communication/RoutingMetadata.cs b/src/ScadaLink.Commons/Messages/Communication/RoutingMetadata.cs new file mode 100644 index 0000000..1801ae4 --- /dev/null +++ b/src/ScadaLink.Commons/Messages/Communication/RoutingMetadata.cs @@ -0,0 +1,6 @@ +namespace ScadaLink.Commons.Messages.Communication; + +public record RoutingMetadata( + string TargetSiteId, + string? TargetInstanceUniqueName, + string CorrelationId); diff --git a/src/ScadaLink.Commons/Messages/Communication/SiteIdentity.cs b/src/ScadaLink.Commons/Messages/Communication/SiteIdentity.cs new file mode 100644 index 0000000..8e7fc63 --- /dev/null +++ b/src/ScadaLink.Commons/Messages/Communication/SiteIdentity.cs @@ -0,0 +1,6 @@ +namespace ScadaLink.Commons.Messages.Communication; + +public record SiteIdentity( + string SiteId, + string NodeHostname, + bool IsActive); diff --git a/src/ScadaLink.Commons/Messages/DebugView/DebugViewSnapshot.cs b/src/ScadaLink.Commons/Messages/DebugView/DebugViewSnapshot.cs new file mode 100644 index 0000000..a09c6f9 --- /dev/null +++ b/src/ScadaLink.Commons/Messages/DebugView/DebugViewSnapshot.cs @@ -0,0 +1,9 @@ +using ScadaLink.Commons.Messages.Streaming; + +namespace ScadaLink.Commons.Messages.DebugView; + +public record DebugViewSnapshot( + string InstanceUniqueName, + IReadOnlyList AttributeValues, + IReadOnlyList AlarmStates, + DateTimeOffset SnapshotTimestamp); diff --git a/src/ScadaLink.Commons/Messages/DebugView/SubscribeDebugViewRequest.cs b/src/ScadaLink.Commons/Messages/DebugView/SubscribeDebugViewRequest.cs new file mode 100644 index 0000000..7bca95e --- /dev/null +++ b/src/ScadaLink.Commons/Messages/DebugView/SubscribeDebugViewRequest.cs @@ -0,0 +1,5 @@ +namespace ScadaLink.Commons.Messages.DebugView; + +public record SubscribeDebugViewRequest( + string InstanceUniqueName, + string CorrelationId); diff --git a/src/ScadaLink.Commons/Messages/DebugView/UnsubscribeDebugViewRequest.cs b/src/ScadaLink.Commons/Messages/DebugView/UnsubscribeDebugViewRequest.cs new file mode 100644 index 0000000..2e6fd83 --- /dev/null +++ b/src/ScadaLink.Commons/Messages/DebugView/UnsubscribeDebugViewRequest.cs @@ -0,0 +1,5 @@ +namespace ScadaLink.Commons.Messages.DebugView; + +public record UnsubscribeDebugViewRequest( + string InstanceUniqueName, + string CorrelationId); diff --git a/src/ScadaLink.Commons/Messages/Deployment/DeployInstanceCommand.cs b/src/ScadaLink.Commons/Messages/Deployment/DeployInstanceCommand.cs new file mode 100644 index 0000000..841ed01 --- /dev/null +++ b/src/ScadaLink.Commons/Messages/Deployment/DeployInstanceCommand.cs @@ -0,0 +1,9 @@ +namespace ScadaLink.Commons.Messages.Deployment; + +public record DeployInstanceCommand( + string DeploymentId, + string InstanceUniqueName, + string RevisionHash, + string FlattenedConfigurationJson, + string DeployedBy, + DateTimeOffset Timestamp); diff --git a/src/ScadaLink.Commons/Messages/Deployment/DeploymentStatusResponse.cs b/src/ScadaLink.Commons/Messages/Deployment/DeploymentStatusResponse.cs new file mode 100644 index 0000000..95ef59d --- /dev/null +++ b/src/ScadaLink.Commons/Messages/Deployment/DeploymentStatusResponse.cs @@ -0,0 +1,10 @@ +using ScadaLink.Commons.Types.Enums; + +namespace ScadaLink.Commons.Messages.Deployment; + +public record DeploymentStatusResponse( + string DeploymentId, + string InstanceUniqueName, + DeploymentStatus Status, + string? ErrorMessage, + DateTimeOffset Timestamp); diff --git a/src/ScadaLink.Commons/Messages/Deployment/DeploymentValidationResult.cs b/src/ScadaLink.Commons/Messages/Deployment/DeploymentValidationResult.cs new file mode 100644 index 0000000..eb6d0b9 --- /dev/null +++ b/src/ScadaLink.Commons/Messages/Deployment/DeploymentValidationResult.cs @@ -0,0 +1,6 @@ +namespace ScadaLink.Commons.Messages.Deployment; + +public record DeploymentValidationResult( + bool IsValid, + IReadOnlyList Errors, + IReadOnlyList Warnings); diff --git a/src/ScadaLink.Commons/Messages/Deployment/FlattenedConfigurationSnapshot.cs b/src/ScadaLink.Commons/Messages/Deployment/FlattenedConfigurationSnapshot.cs new file mode 100644 index 0000000..f6c4c11 --- /dev/null +++ b/src/ScadaLink.Commons/Messages/Deployment/FlattenedConfigurationSnapshot.cs @@ -0,0 +1,7 @@ +namespace ScadaLink.Commons.Messages.Deployment; + +public record FlattenedConfigurationSnapshot( + string InstanceUniqueName, + string RevisionHash, + string ConfigurationJson, + DateTimeOffset GeneratedAt); diff --git a/src/ScadaLink.Commons/Messages/Health/HeartbeatMessage.cs b/src/ScadaLink.Commons/Messages/Health/HeartbeatMessage.cs new file mode 100644 index 0000000..4c53e74 --- /dev/null +++ b/src/ScadaLink.Commons/Messages/Health/HeartbeatMessage.cs @@ -0,0 +1,7 @@ +namespace ScadaLink.Commons.Messages.Health; + +public record HeartbeatMessage( + string SiteId, + string NodeHostname, + bool IsActive, + DateTimeOffset Timestamp); diff --git a/src/ScadaLink.Commons/Messages/Health/SiteHealthReport.cs b/src/ScadaLink.Commons/Messages/Health/SiteHealthReport.cs new file mode 100644 index 0000000..bdd8af1 --- /dev/null +++ b/src/ScadaLink.Commons/Messages/Health/SiteHealthReport.cs @@ -0,0 +1,14 @@ +using ScadaLink.Commons.Types.Enums; + +namespace ScadaLink.Commons.Messages.Health; + +public record SiteHealthReport( + string SiteId, + long SequenceNumber, + DateTimeOffset ReportTimestamp, + IReadOnlyDictionary DataConnectionStatuses, + IReadOnlyDictionary TagResolutionCounts, + int ScriptErrorCount, + int AlarmEvaluationErrorCount, + IReadOnlyDictionary StoreAndForwardBufferDepths, + int DeadLetterCount); diff --git a/src/ScadaLink.Commons/Messages/Health/TagResolutionStatus.cs b/src/ScadaLink.Commons/Messages/Health/TagResolutionStatus.cs new file mode 100644 index 0000000..f07030b --- /dev/null +++ b/src/ScadaLink.Commons/Messages/Health/TagResolutionStatus.cs @@ -0,0 +1,3 @@ +namespace ScadaLink.Commons.Messages.Health; + +public record TagResolutionStatus(int TotalSubscribed, int SuccessfullyResolved); diff --git a/src/ScadaLink.Commons/Messages/Lifecycle/DeleteInstanceCommand.cs b/src/ScadaLink.Commons/Messages/Lifecycle/DeleteInstanceCommand.cs new file mode 100644 index 0000000..f4e2b65 --- /dev/null +++ b/src/ScadaLink.Commons/Messages/Lifecycle/DeleteInstanceCommand.cs @@ -0,0 +1,6 @@ +namespace ScadaLink.Commons.Messages.Lifecycle; + +public record DeleteInstanceCommand( + string CommandId, + string InstanceUniqueName, + DateTimeOffset Timestamp); diff --git a/src/ScadaLink.Commons/Messages/Lifecycle/DisableInstanceCommand.cs b/src/ScadaLink.Commons/Messages/Lifecycle/DisableInstanceCommand.cs new file mode 100644 index 0000000..1e6e0af --- /dev/null +++ b/src/ScadaLink.Commons/Messages/Lifecycle/DisableInstanceCommand.cs @@ -0,0 +1,6 @@ +namespace ScadaLink.Commons.Messages.Lifecycle; + +public record DisableInstanceCommand( + string CommandId, + string InstanceUniqueName, + DateTimeOffset Timestamp); diff --git a/src/ScadaLink.Commons/Messages/Lifecycle/EnableInstanceCommand.cs b/src/ScadaLink.Commons/Messages/Lifecycle/EnableInstanceCommand.cs new file mode 100644 index 0000000..a85c7a3 --- /dev/null +++ b/src/ScadaLink.Commons/Messages/Lifecycle/EnableInstanceCommand.cs @@ -0,0 +1,6 @@ +namespace ScadaLink.Commons.Messages.Lifecycle; + +public record EnableInstanceCommand( + string CommandId, + string InstanceUniqueName, + DateTimeOffset Timestamp); diff --git a/src/ScadaLink.Commons/Messages/Lifecycle/InstanceLifecycleResponse.cs b/src/ScadaLink.Commons/Messages/Lifecycle/InstanceLifecycleResponse.cs new file mode 100644 index 0000000..db05295 --- /dev/null +++ b/src/ScadaLink.Commons/Messages/Lifecycle/InstanceLifecycleResponse.cs @@ -0,0 +1,8 @@ +namespace ScadaLink.Commons.Messages.Lifecycle; + +public record InstanceLifecycleResponse( + string CommandId, + string InstanceUniqueName, + bool Success, + string? ErrorMessage, + DateTimeOffset Timestamp); diff --git a/src/ScadaLink.Commons/Messages/ScriptExecution/ScriptCallRequest.cs b/src/ScadaLink.Commons/Messages/ScriptExecution/ScriptCallRequest.cs new file mode 100644 index 0000000..5d06582 --- /dev/null +++ b/src/ScadaLink.Commons/Messages/ScriptExecution/ScriptCallRequest.cs @@ -0,0 +1,7 @@ +namespace ScadaLink.Commons.Messages.ScriptExecution; + +public record ScriptCallRequest( + string ScriptName, + IReadOnlyDictionary? Parameters, + int CurrentCallDepth, + string CorrelationId); diff --git a/src/ScadaLink.Commons/Messages/ScriptExecution/ScriptCallResult.cs b/src/ScadaLink.Commons/Messages/ScriptExecution/ScriptCallResult.cs new file mode 100644 index 0000000..00412dd --- /dev/null +++ b/src/ScadaLink.Commons/Messages/ScriptExecution/ScriptCallResult.cs @@ -0,0 +1,7 @@ +namespace ScadaLink.Commons.Messages.ScriptExecution; + +public record ScriptCallResult( + string CorrelationId, + bool Success, + object? ReturnValue, + string? ErrorMessage); diff --git a/src/ScadaLink.Commons/Messages/Streaming/AlarmStateChanged.cs b/src/ScadaLink.Commons/Messages/Streaming/AlarmStateChanged.cs new file mode 100644 index 0000000..ad7de6e --- /dev/null +++ b/src/ScadaLink.Commons/Messages/Streaming/AlarmStateChanged.cs @@ -0,0 +1,10 @@ +using ScadaLink.Commons.Types.Enums; + +namespace ScadaLink.Commons.Messages.Streaming; + +public record AlarmStateChanged( + string InstanceUniqueName, + string AlarmName, + AlarmState State, + int Priority, + DateTimeOffset Timestamp); diff --git a/src/ScadaLink.Commons/Messages/Streaming/AttributeValueChanged.cs b/src/ScadaLink.Commons/Messages/Streaming/AttributeValueChanged.cs new file mode 100644 index 0000000..64680c9 --- /dev/null +++ b/src/ScadaLink.Commons/Messages/Streaming/AttributeValueChanged.cs @@ -0,0 +1,9 @@ +namespace ScadaLink.Commons.Messages.Streaming; + +public record AttributeValueChanged( + string InstanceUniqueName, + string AttributePath, + string AttributeName, + object? Value, + string Quality, + DateTimeOffset Timestamp); diff --git a/src/ScadaLink.Commons/Types/Enums/AlarmState.cs b/src/ScadaLink.Commons/Types/Enums/AlarmState.cs new file mode 100644 index 0000000..1df94bb --- /dev/null +++ b/src/ScadaLink.Commons/Types/Enums/AlarmState.cs @@ -0,0 +1,7 @@ +namespace ScadaLink.Commons.Types.Enums; + +public enum AlarmState +{ + Active, + Normal +} diff --git a/src/ScadaLink.Commons/Types/Enums/AlarmTriggerType.cs b/src/ScadaLink.Commons/Types/Enums/AlarmTriggerType.cs new file mode 100644 index 0000000..314e006 --- /dev/null +++ b/src/ScadaLink.Commons/Types/Enums/AlarmTriggerType.cs @@ -0,0 +1,8 @@ +namespace ScadaLink.Commons.Types.Enums; + +public enum AlarmTriggerType +{ + ValueMatch, + RangeViolation, + RateOfChange +} diff --git a/src/ScadaLink.Commons/Types/Enums/ConnectionHealth.cs b/src/ScadaLink.Commons/Types/Enums/ConnectionHealth.cs new file mode 100644 index 0000000..03fee5c --- /dev/null +++ b/src/ScadaLink.Commons/Types/Enums/ConnectionHealth.cs @@ -0,0 +1,9 @@ +namespace ScadaLink.Commons.Types.Enums; + +public enum ConnectionHealth +{ + Connected, + Disconnected, + Connecting, + Error +} diff --git a/src/ScadaLink.Commons/Types/Enums/DataType.cs b/src/ScadaLink.Commons/Types/Enums/DataType.cs new file mode 100644 index 0000000..3ae2dbf --- /dev/null +++ b/src/ScadaLink.Commons/Types/Enums/DataType.cs @@ -0,0 +1,12 @@ +namespace ScadaLink.Commons.Types.Enums; + +public enum DataType +{ + Boolean, + Int32, + Float, + Double, + String, + DateTime, + Binary +} diff --git a/src/ScadaLink.Commons/Types/Enums/DeploymentStatus.cs b/src/ScadaLink.Commons/Types/Enums/DeploymentStatus.cs new file mode 100644 index 0000000..5a64e2f --- /dev/null +++ b/src/ScadaLink.Commons/Types/Enums/DeploymentStatus.cs @@ -0,0 +1,9 @@ +namespace ScadaLink.Commons.Types.Enums; + +public enum DeploymentStatus +{ + Pending, + InProgress, + Success, + Failed +} diff --git a/src/ScadaLink.Commons/Types/Enums/InstanceState.cs b/src/ScadaLink.Commons/Types/Enums/InstanceState.cs new file mode 100644 index 0000000..a331955 --- /dev/null +++ b/src/ScadaLink.Commons/Types/Enums/InstanceState.cs @@ -0,0 +1,7 @@ +namespace ScadaLink.Commons.Types.Enums; + +public enum InstanceState +{ + Enabled, + Disabled +} diff --git a/src/ScadaLink.Commons/Types/Result.cs b/src/ScadaLink.Commons/Types/Result.cs new file mode 100644 index 0000000..791ef32 --- /dev/null +++ b/src/ScadaLink.Commons/Types/Result.cs @@ -0,0 +1,40 @@ +namespace ScadaLink.Commons.Types; + +public sealed class Result +{ + private readonly T? _value; + private readonly string? _error; + + private Result(T value) + { + _value = value; + _error = null; + IsSuccess = true; + } + + private Result(string error) + { + _value = default; + _error = error; + IsSuccess = false; + } + + public bool IsSuccess { get; } + + public bool IsFailure => !IsSuccess; + + public T Value => IsSuccess + ? _value! + : throw new InvalidOperationException("Cannot access Value on a failed Result. Error: " + _error); + + public string Error => IsFailure + ? _error! + : throw new InvalidOperationException("Cannot access Error on a successful Result."); + + public static Result Success(T value) => new(value); + + public static Result Failure(string error) => new(error); + + public TResult Match(Func onSuccess, Func onFailure) => + IsSuccess ? onSuccess(_value!) : onFailure(_error!); +} diff --git a/src/ScadaLink.Commons/Types/RetryPolicy.cs b/src/ScadaLink.Commons/Types/RetryPolicy.cs new file mode 100644 index 0000000..5dcb76f --- /dev/null +++ b/src/ScadaLink.Commons/Types/RetryPolicy.cs @@ -0,0 +1,3 @@ +namespace ScadaLink.Commons.Types; + +public record RetryPolicy(int MaxRetries, TimeSpan Delay); diff --git a/tests/ScadaLink.Commons.Tests/ArchitecturalConstraintTests.cs b/tests/ScadaLink.Commons.Tests/ArchitecturalConstraintTests.cs new file mode 100644 index 0000000..356ff54 --- /dev/null +++ b/tests/ScadaLink.Commons.Tests/ArchitecturalConstraintTests.cs @@ -0,0 +1,187 @@ +using System.Reflection; +using System.Xml.Linq; + +namespace ScadaLink.Commons.Tests; + +public class ArchitecturalConstraintTests +{ + private static readonly Assembly CommonsAssembly = typeof(ScadaLink.Commons.Types.RetryPolicy).Assembly; + + private static string GetCsprojPath() + { + // Walk up from test output to find the Commons csproj + var dir = new DirectoryInfo(AppContext.BaseDirectory); + while (dir != null) + { + var candidate = Path.Combine(dir.FullName, "src", "ScadaLink.Commons", "ScadaLink.Commons.csproj"); + if (File.Exists(candidate)) + return candidate; + dir = dir.Parent; + } + throw new InvalidOperationException("Could not find ScadaLink.Commons.csproj"); + } + + private static string GetCommonsSourceDirectory() + { + var csproj = GetCsprojPath(); + return Path.GetDirectoryName(csproj)!; + } + + [Fact] + public void Csproj_ShouldNotHaveForbiddenPackageReferences() + { + var csprojPath = GetCsprojPath(); + var doc = XDocument.Load(csprojPath); + var ns = doc.Root!.GetDefaultNamespace(); + + var forbiddenPrefixes = new[] { "Akka.", "Microsoft.AspNetCore.", "Microsoft.EntityFrameworkCore." }; + + var packageRefs = doc.Descendants(ns + "PackageReference") + .Select(e => e.Attribute("Include")?.Value) + .Where(v => v != null) + .ToList(); + + foreach (var pkg in packageRefs) + { + foreach (var prefix in forbiddenPrefixes) + { + Assert.False(pkg!.StartsWith(prefix, StringComparison.OrdinalIgnoreCase), + $"Commons has forbidden package reference: {pkg}"); + } + } + } + + [Fact] + public void Assembly_ShouldNotReferenceForbiddenAssemblies() + { + var forbiddenPrefixes = new[] { "Akka.", "Microsoft.AspNetCore.", "Microsoft.EntityFrameworkCore." }; + + var referencedAssemblies = CommonsAssembly.GetReferencedAssemblies(); + + foreach (var asmName in referencedAssemblies) + { + foreach (var prefix in forbiddenPrefixes) + { + Assert.False(asmName.Name!.StartsWith(prefix, StringComparison.OrdinalIgnoreCase), + $"Commons references forbidden assembly: {asmName.Name}"); + } + } + } + + [Fact] + public void Commons_ShouldNotContainServiceOrActorImplementations() + { + // Heuristic: class has > 3 public non-property methods that are not constructors + var types = CommonsAssembly.GetTypes() + .Where(t => t.IsClass && !t.IsAbstract && !t.IsInterface); + + foreach (var type in types) + { + var publicMethods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) + .Where(m => !m.IsSpecialName) // excludes property getters/setters + .Where(m => !m.Name.StartsWith("<")) // excludes compiler-generated + .Where(m => m.Name != "ToString" && m.Name != "GetHashCode" && + m.Name != "Equals" && m.Name != "Deconstruct" && + m.Name != "PrintMembers" && m.Name != "GetType") + .ToList(); + + Assert.True(publicMethods.Count <= 3, + $"Type {type.FullName} has {publicMethods.Count} public methods ({string.Join(", ", publicMethods.Select(m => m.Name))}), which suggests it may contain service/actor logic"); + } + } + + [Fact] + public void SourceFiles_ShouldNotContainToLocalTime() + { + var sourceDir = GetCommonsSourceDirectory(); + var csFiles = Directory.GetFiles(sourceDir, "*.cs", SearchOption.AllDirectories); + + foreach (var file in csFiles) + { + var content = File.ReadAllText(file); + Assert.DoesNotContain("ToLocalTime()", content); + } + } + + [Fact] + public void AllEntities_ShouldHaveNoEfAttributes() + { + var efAttributeNames = new HashSet + { + "KeyAttribute", "ForeignKeyAttribute", "TableAttribute", + "ColumnAttribute", "RequiredAttribute", "MaxLengthAttribute", + "StringLengthAttribute", "DatabaseGeneratedAttribute", + "NotMappedAttribute", "IndexAttribute", "InversePropertyAttribute" + }; + + var entityTypes = CommonsAssembly.GetTypes() + .Where(t => t.IsClass && !t.IsAbstract && t.Namespace != null + && t.Namespace.Contains(".Entities.")); + + foreach (var type in entityTypes) + { + foreach (var attr in type.GetCustomAttributes(true)) + { + Assert.DoesNotContain(attr.GetType().Name, efAttributeNames); + } + + foreach (var prop in type.GetProperties()) + { + foreach (var attr in prop.GetCustomAttributes(true)) + { + Assert.False(efAttributeNames.Contains(attr.GetType().Name), + $"{type.Name}.{prop.Name} has EF attribute {attr.GetType().Name}"); + } + } + } + } + + [Fact] + public void AllEnums_ShouldBeSingularNamed() + { + var enums = CommonsAssembly.GetTypes().Where(t => t.IsEnum); + + foreach (var enumType in enums) + { + var name = enumType.Name; + // Singular names should not end with 's' (except words ending in 'ss' or 'us' which are singular) + Assert.False(name.EndsWith("s") && !name.EndsWith("ss") && !name.EndsWith("us"), + $"Enum {name} appears to be plural"); + } + } + + [Fact] + public void AllMessageTypes_ShouldBeRecords() + { + var messageTypes = CommonsAssembly.GetTypes() + .Where(t => t.Namespace != null + && t.Namespace.Contains(".Messages.") + && !t.IsEnum && !t.IsInterface + && (t.IsClass || (t.IsValueType && !t.IsPrimitive))); + + foreach (var type in messageTypes) + { + 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 KeyEntities_ShouldHaveDateTimeOffsetTimestamps() + { + // Spot-check key entity timestamp properties + var deploymentRecord = CommonsAssembly.GetType("ScadaLink.Commons.Entities.Deployment.DeploymentRecord"); + Assert.NotNull(deploymentRecord); + Assert.Equal(typeof(DateTimeOffset), deploymentRecord!.GetProperty("DeployedAt")!.PropertyType); + Assert.Equal(typeof(DateTimeOffset?), deploymentRecord.GetProperty("CompletedAt")!.PropertyType); + + var artifactRecord = CommonsAssembly.GetType("ScadaLink.Commons.Entities.Deployment.SystemArtifactDeploymentRecord"); + Assert.NotNull(artifactRecord); + Assert.Equal(typeof(DateTimeOffset), artifactRecord!.GetProperty("DeployedAt")!.PropertyType); + + var auditEntry = CommonsAssembly.GetType("ScadaLink.Commons.Entities.Audit.AuditLogEntry"); + Assert.NotNull(auditEntry); + Assert.Equal(typeof(DateTimeOffset), auditEntry!.GetProperty("Timestamp")!.PropertyType); + } +} diff --git a/tests/ScadaLink.Commons.Tests/Entities/EntityConventionTests.cs b/tests/ScadaLink.Commons.Tests/Entities/EntityConventionTests.cs new file mode 100644 index 0000000..084c516 --- /dev/null +++ b/tests/ScadaLink.Commons.Tests/Entities/EntityConventionTests.cs @@ -0,0 +1,89 @@ +using System.Collections; +using System.Reflection; + +namespace ScadaLink.Commons.Tests.Entities; + +public class EntityConventionTests +{ + private static readonly Assembly CommonsAssembly = typeof(ScadaLink.Commons.Types.RetryPolicy).Assembly; + + private static IEnumerable GetEntityTypes() => + CommonsAssembly.GetTypes() + .Where(t => t.IsClass && !t.IsAbstract && t.Namespace != null + && t.Namespace.Contains(".Entities.")); + + [Fact] + public void AllEntities_ShouldHaveNoEfAttributes() + { + var efAttributeNames = new HashSet + { + "KeyAttribute", "ForeignKeyAttribute", "TableAttribute", + "ColumnAttribute", "RequiredAttribute", "MaxLengthAttribute", + "StringLengthAttribute", "DatabaseGeneratedAttribute", + "NotMappedAttribute", "IndexAttribute", "InversePropertyAttribute" + }; + + foreach (var entityType in GetEntityTypes()) + { + // Check class-level attributes + var classAttrs = entityType.GetCustomAttributes(true); + foreach (var attr in classAttrs) + { + Assert.DoesNotContain(attr.GetType().Name, efAttributeNames); + } + + // Check property-level attributes + foreach (var prop in entityType.GetProperties()) + { + var propAttrs = prop.GetCustomAttributes(true); + foreach (var attr in propAttrs) + { + Assert.False(efAttributeNames.Contains(attr.GetType().Name), + $"Entity {entityType.Name}.{prop.Name} has EF attribute {attr.GetType().Name}"); + } + } + } + } + + [Fact] + public void AllTimestampProperties_ShouldBeDateTimeOffset() + { + var timestampNames = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "Timestamp", "DeployedAt", "CompletedAt", "GeneratedAt", + "ReportTimestamp", "SnapshotTimestamp" + }; + + foreach (var entityType in GetEntityTypes()) + { + foreach (var prop in entityType.GetProperties()) + { + if (timestampNames.Contains(prop.Name)) + { + var underlyingType = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType; + Assert.True(underlyingType == typeof(DateTimeOffset), + $"{entityType.Name}.{prop.Name} should be DateTimeOffset but is {prop.PropertyType.Name}"); + } + } + } + } + + [Fact] + public void NavigationProperties_ShouldBeICollection() + { + foreach (var entityType in GetEntityTypes()) + { + foreach (var prop in entityType.GetProperties()) + { + if (prop.PropertyType.IsGenericType && + typeof(IEnumerable).IsAssignableFrom(prop.PropertyType) && + prop.PropertyType != typeof(string)) + { + Assert.True( + prop.PropertyType.GetGenericTypeDefinition() == typeof(ICollection<>), + $"{entityType.Name}.{prop.Name} should be ICollection but is {prop.PropertyType.Name}"); + } + } + } + } +} diff --git a/tests/ScadaLink.Commons.Tests/Messages/MessageConventionTests.cs b/tests/ScadaLink.Commons.Tests/Messages/MessageConventionTests.cs new file mode 100644 index 0000000..5932e11 --- /dev/null +++ b/tests/ScadaLink.Commons.Tests/Messages/MessageConventionTests.cs @@ -0,0 +1,130 @@ +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); + + 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, + 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!); + } +} diff --git a/tests/ScadaLink.Commons.Tests/Types/EnumTests.cs b/tests/ScadaLink.Commons.Tests/Types/EnumTests.cs new file mode 100644 index 0000000..a205cef --- /dev/null +++ b/tests/ScadaLink.Commons.Tests/Types/EnumTests.cs @@ -0,0 +1,36 @@ +using ScadaLink.Commons.Types.Enums; + +namespace ScadaLink.Commons.Tests.Types; + +public class EnumTests +{ + [Theory] + [InlineData(typeof(DataType), new[] { "Boolean", "Int32", "Float", "Double", "String", "DateTime", "Binary" })] + [InlineData(typeof(InstanceState), new[] { "Enabled", "Disabled" })] + [InlineData(typeof(DeploymentStatus), new[] { "Pending", "InProgress", "Success", "Failed" })] + [InlineData(typeof(AlarmState), new[] { "Active", "Normal" })] + [InlineData(typeof(AlarmTriggerType), new[] { "ValueMatch", "RangeViolation", "RateOfChange" })] + [InlineData(typeof(ConnectionHealth), new[] { "Connected", "Disconnected", "Connecting", "Error" })] + public void Enum_ShouldHaveExpectedValues(Type enumType, string[] expectedNames) + { + var actualNames = Enum.GetNames(enumType); + Assert.Equal(expectedNames, actualNames); + } + + [Theory] + [InlineData(typeof(DataType))] + [InlineData(typeof(InstanceState))] + [InlineData(typeof(DeploymentStatus))] + [InlineData(typeof(AlarmState))] + [InlineData(typeof(AlarmTriggerType))] + [InlineData(typeof(ConnectionHealth))] + public void Enum_ShouldBeSingularNamed(Type enumType) + { + // Singular names should not end with 's' (except 'Status' which is singular) + var name = enumType.Name; + Assert.False(name.EndsWith("es") && !name.EndsWith("Status"), + $"Enum {name} appears to be plural (ends with 'es')."); + Assert.False(name.EndsWith("s") && !name.EndsWith("ss") && !name.EndsWith("us"), + $"Enum {name} appears to be plural (ends with 's')."); + } +} diff --git a/tests/ScadaLink.Commons.Tests/Types/ResultTests.cs b/tests/ScadaLink.Commons.Tests/Types/ResultTests.cs new file mode 100644 index 0000000..17c286f --- /dev/null +++ b/tests/ScadaLink.Commons.Tests/Types/ResultTests.cs @@ -0,0 +1,75 @@ +using ScadaLink.Commons.Types; + +namespace ScadaLink.Commons.Tests.Types; + +public class ResultTests +{ + [Fact] + public void Success_ShouldCreateSuccessfulResult() + { + var result = Result.Success(42); + + Assert.True(result.IsSuccess); + Assert.False(result.IsFailure); + Assert.Equal(42, result.Value); + } + + [Fact] + public void Failure_ShouldCreateFailedResult() + { + var result = Result.Failure("something went wrong"); + + Assert.True(result.IsFailure); + Assert.False(result.IsSuccess); + Assert.Equal("something went wrong", result.Error); + } + + [Fact] + public void Value_OnFailure_ShouldThrow() + { + var result = Result.Failure("error"); + + Assert.Throws(() => result.Value); + } + + [Fact] + public void Error_OnSuccess_ShouldThrow() + { + var result = Result.Success(42); + + Assert.Throws(() => result.Error); + } + + [Fact] + public void Match_OnSuccess_ShouldCallOnSuccess() + { + var result = Result.Success(42); + + var output = result.Match( + v => $"value={v}", + e => $"error={e}"); + + Assert.Equal("value=42", output); + } + + [Fact] + public void Match_OnFailure_ShouldCallOnFailure() + { + var result = Result.Failure("bad"); + + var output = result.Match( + v => $"value={v}", + e => $"error={e}"); + + Assert.Equal("error=bad", output); + } + + [Fact] + public void Success_WithNullableReferenceType_ShouldWork() + { + var result = Result.Success("hello"); + + Assert.True(result.IsSuccess); + Assert.Equal("hello", result.Value); + } +} diff --git a/tests/ScadaLink.Commons.Tests/Types/RetryPolicyTests.cs b/tests/ScadaLink.Commons.Tests/Types/RetryPolicyTests.cs new file mode 100644 index 0000000..2c495ee --- /dev/null +++ b/tests/ScadaLink.Commons.Tests/Types/RetryPolicyTests.cs @@ -0,0 +1,35 @@ +using ScadaLink.Commons.Types; + +namespace ScadaLink.Commons.Tests.Types; + +public class RetryPolicyTests +{ + [Fact] + public void RetryPolicy_ShouldBeImmutableRecord() + { + var policy = new RetryPolicy(3, TimeSpan.FromSeconds(5)); + + Assert.Equal(3, policy.MaxRetries); + Assert.Equal(TimeSpan.FromSeconds(5), policy.Delay); + } + + [Fact] + public void RetryPolicy_WithExpression_ShouldCreateNewInstance() + { + var original = new RetryPolicy(3, TimeSpan.FromSeconds(5)); + var modified = original with { MaxRetries = 5 }; + + Assert.Equal(3, original.MaxRetries); + Assert.Equal(5, modified.MaxRetries); + Assert.Equal(original.Delay, modified.Delay); + } + + [Fact] + public void RetryPolicy_EqualValues_ShouldBeEqual() + { + var a = new RetryPolicy(3, TimeSpan.FromSeconds(5)); + var b = new RetryPolicy(3, TimeSpan.FromSeconds(5)); + + Assert.Equal(a, b); + } +} diff --git a/tests/ScadaLink.Commons.Tests/UnitTest1.cs b/tests/ScadaLink.Commons.Tests/UnitTest1.cs deleted file mode 100644 index 5253d8b..0000000 --- a/tests/ScadaLink.Commons.Tests/UnitTest1.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace ScadaLink.Commons.Tests; - -public class UnitTest1 -{ - [Fact] - public void Test1() - { - - } -}