f4709ff143
Plan: docs/plans/speculative-items-sweep.md (also covers parallelism + findings). Implemented: - C3: HistorianTagDefinition.IntegralDivisor (default 1.0). Wire bytes flip per the captured native serializer; live probe shows the server stores IntegralDivisor on EngineeringUnit (shared) rather than per-tag, so the value is accepted on the wire but doesn't visibly persist for the test EU. Documented in the property's doc-comment. - E: HistorianWcfCertOptionTests (5 tests) covering AllowUntrustedServer- Certificate validator installation + ServerDnsIdentity propagation through CreateEndpointAddress and CreateBindingPair. Investigated + documented (deferred): - D3: Discrete/String/Int1/Int8/UInt8 EnsT2 root cause — server-side ValidationFailed: "Transaction validation failed". Native AddTag's validator rejects non-analog types; not a wire-format issue. To unlock, need to capture a working native flow via a different code path (likely SMC's tag-import path or AddTagExtendedProperties carrying type-specific metadata). Defer until a customer asks. - D1: AddTagExtendedProperties feasibility — managed surface confirmed (ArchestrA.HistorianAccess.AddTagExtendedProperties + WCF op AddTagExtendedPropertyGroups). Cost estimated at 1-2 days of focused RE work due to CTagExtendedPropertyGroup payload complexity. Defer. - D2: AddRevisionValuesBegin/Value/End — sub-plan written at docs/plans/revision-write-path.md with 5-step capture sequence, workstream estimates, and risk register. Implementation deferred. 177/177 tests pass (was 172; +5 cert tests + 1 IntegralDivisor unit test, harness probe results not committed). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
98 lines
4.1 KiB
C#
98 lines
4.1 KiB
C#
using System.IdentityModel.Selectors;
|
|
using System.Security.Cryptography.X509Certificates;
|
|
using System.ServiceModel;
|
|
using System.ServiceModel.Channels;
|
|
using System.ServiceModel.Security;
|
|
using AVEVA.Historian.Client;
|
|
using AVEVA.Historian.Client.Wcf;
|
|
using AVEVA.Historian.Client.Wcf.Contracts;
|
|
|
|
namespace AVEVA.Historian.Client.Tests;
|
|
|
|
public sealed class HistorianWcfCertOptionTests
|
|
{
|
|
private static HistorianClientOptions BaseOptions(bool allowUntrusted = false, string? dnsIdentity = null) =>
|
|
new()
|
|
{
|
|
Host = "10.0.0.1",
|
|
Port = HistorianClientOptions.DefaultPort,
|
|
Transport = HistorianTransport.RemoteTcpCertificate,
|
|
IntegratedSecurity = false,
|
|
UserName = "user",
|
|
Password = "pass",
|
|
AllowUntrustedServerCertificate = allowUntrusted,
|
|
ServerDnsIdentity = dnsIdentity,
|
|
};
|
|
|
|
[Fact]
|
|
public void ClientCredentialsHelper_Disabled_LeavesValidationModeAtDefault()
|
|
{
|
|
Binding binding = HistorianWcfBindingFactory.CreateMdasNetTcpBinding(TimeSpan.FromSeconds(5));
|
|
ChannelFactory<IHistoryServiceContract2> factory = new(binding, new EndpointAddress("net.tcp://10.0.0.1:32568/Hist"));
|
|
try
|
|
{
|
|
HistorianWcfClientCredentialsHelper.Configure(factory, BaseOptions(allowUntrusted: false));
|
|
|
|
X509ServiceCertificateAuthentication auth = factory.Credentials.ServiceCertificate.SslCertificateAuthentication
|
|
?? factory.Credentials.ServiceCertificate.Authentication;
|
|
// Default validation mode is ChainTrust — explicitly NOT None / Custom.
|
|
Assert.NotEqual(X509CertificateValidationMode.None, auth.CertificateValidationMode);
|
|
Assert.Null(auth.CustomCertificateValidator);
|
|
}
|
|
finally
|
|
{
|
|
factory.Abort();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void ClientCredentialsHelper_Enabled_InstallsAcceptAnyValidator()
|
|
{
|
|
Binding binding = HistorianWcfBindingFactory.CreateMdasNetTcpBinding(TimeSpan.FromSeconds(5));
|
|
ChannelFactory<IHistoryServiceContract2> factory = new(binding, new EndpointAddress("net.tcp://10.0.0.1:32568/Hist"));
|
|
try
|
|
{
|
|
HistorianWcfClientCredentialsHelper.Configure(factory, BaseOptions(allowUntrusted: true));
|
|
|
|
X509ServiceCertificateAuthentication auth = factory.Credentials.ServiceCertificate.SslCertificateAuthentication;
|
|
Assert.NotNull(auth);
|
|
Assert.Equal(X509CertificateValidationMode.Custom, auth.CertificateValidationMode);
|
|
Assert.Equal(X509RevocationMode.NoCheck, auth.RevocationMode);
|
|
Assert.NotNull(auth.CustomCertificateValidator);
|
|
Assert.IsAssignableFrom<X509CertificateValidator>(auth.CustomCertificateValidator);
|
|
}
|
|
finally
|
|
{
|
|
factory.Abort();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void CreateEndpointAddress_WithoutDnsIdentity_HasNullIdentity()
|
|
{
|
|
EndpointAddress address = HistorianWcfBindingFactory.CreateEndpointAddress("10.0.0.1", 32568, "Hist");
|
|
Assert.Null(address.Identity);
|
|
}
|
|
|
|
[Fact]
|
|
public void CreateEndpointAddress_WithDnsIdentity_AttachesDnsEndpointIdentity()
|
|
{
|
|
EndpointAddress address = HistorianWcfBindingFactory.CreateEndpointAddress("10.0.0.1", 32568, "HistCert", "localhost");
|
|
Assert.NotNull(address.Identity);
|
|
DnsEndpointIdentity dns = Assert.IsType<DnsEndpointIdentity>(address.Identity);
|
|
Assert.Equal("localhost", dns.IdentityClaim.Resource);
|
|
}
|
|
|
|
[Fact]
|
|
public void CreateBindingPair_RemoteTcpCertificate_PropagatesServerDnsIdentity()
|
|
{
|
|
HistorianClientOptions options = BaseOptions(dnsIdentity: "localhost");
|
|
var (_, historyEndpoint, _, retrievalEndpoint) = HistorianWcfBindingFactory.CreateBindingPair(options);
|
|
|
|
DnsEndpointIdentity historyIdentity = Assert.IsType<DnsEndpointIdentity>(historyEndpoint.Identity);
|
|
Assert.Equal("localhost", historyIdentity.IdentityClaim.Resource);
|
|
// The Retrieval endpoint uses plain MdasNetTcp without TLS — no DNS identity needed.
|
|
Assert.Null(retrievalEndpoint.Identity);
|
|
}
|
|
}
|