Remove dead dialect methods; unblock explicit-creds tag-metadata path
Two cleanups from the post-EnsureTagAsync punch list — both isolated, no protocol discovery required. #89 dead code in Historian2020ProtocolDialect: - BrowseTagNamesAsync and GetTagMetadataAsync on the dialect both threw ProtocolEvidenceMissingException, but HistorianClient routes those calls directly to HistorianWcfTagClient — the dialect overrides were never reached. Removed both methods. ReadBlocksAsync stays (it's a deliberate guardrailed entry on the public surface). #90 explicit-creds tag-metadata path: - HistorianWcfTagClient.WcfRetrievalSession.ValidateSupportedAuth threw ProtocolEvidenceMissingException whenever IntegratedSecurity=false AND UserName/Password were supplied. But the surrounding code already wires those creds through ApplyWindowsCredential -> factory.Credentials.Windows.ClientCredential — the validator was just being conservative about an untested combination. - Inverted the check: now only rejects the no-auth-at-all combination (IntegratedSecurity=false + no UserName + no Password). The other three valid auth shapes pass through to WCF. Tests: 161 -> 163 (+2). New unit test verifies the no-auth case still throws; new gated live integration test GetTagMetadataAsync_ExplicitCredentials_AgainstLocalHistorian exercises the explicit-creds path when HISTORIAN_USER+HISTORIAN_PASSWORD are set, skips cleanly otherwise. CLAUDE.md updated: removed the two now-resolved entries from "Remaining gaps"; explicit-creds line refined to note the live-verification env-var requirement. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -94,8 +94,7 @@ End-to-end chain working from a pure managed .NET 10 client: `Hist.GetV → Hist
|
||||
Smaller, isolated items — none block the production read surface:
|
||||
|
||||
- Remote TCP transports (`RemoteTcpIntegrated`, `RemoteTcpCertificate`) untested against an actual remote Historian (tests skip without `HISTORIAN_REMOTE_TCP_HOST`).
|
||||
- Explicit username/password tag-metadata path (`HistorianWcfTagClient` line ~357) throws — only integrated security wired for that op.
|
||||
- `Historian2020ProtocolDialect.GetTagInfoByName/GetTagInfos` throws — currently dead code; `GetTagMetadataAsync` works through the WCF tag client instead.
|
||||
- Explicit username/password tag-metadata path is wired (validator only blocks no-auth-at-all), but live-verification requires `HISTORIAN_USER`+`HISTORIAN_PASSWORD` set; gated test `GetTagMetadataAsync_ExplicitCredentials_AgainstLocalHistorian` skips otherwise.
|
||||
- Per-row trailing ~24 bytes of `GetNextQueryResultBuffer` are not decoded (likely per-sample value/source/state metadata).
|
||||
- `EnsureTagAsync` distinct `MinRaw`/`MaxRaw` persistence requires `ApplyScaling=true` + a follow-up `UpdateTags` call — not yet wired (no API user has asked).
|
||||
|
||||
|
||||
@@ -87,17 +87,6 @@ internal sealed class Historian2020ProtocolDialect
|
||||
#pragma warning restore CA1416
|
||||
}
|
||||
|
||||
public IAsyncEnumerable<string> BrowseTagNamesAsync(string filter, CancellationToken cancellationToken)
|
||||
{
|
||||
return Missing<string>("StartLikeTagNameSearch/GetLikeTagnames", cancellationToken);
|
||||
}
|
||||
|
||||
public Task<HistorianTagMetadata?> GetTagMetadataAsync(string tag, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
throw new ProtocolEvidenceMissingException("GetTagInfoByName/GetTagInfos");
|
||||
}
|
||||
|
||||
public Task<HistorianConnectionStatus> GetConnectionStatusAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
@@ -351,10 +351,18 @@ internal static class HistorianWcfTagClient
|
||||
|
||||
private static void ValidateSupportedAuth(HistorianClientOptions options)
|
||||
{
|
||||
// Three valid auth shapes:
|
||||
// 1. IntegratedSecurity=true (current Windows identity, no UserName/Password)
|
||||
// 2. IntegratedSecurity=false + UserName + Password (NTLM/Kerberos with explicit creds)
|
||||
// 3. IntegratedSecurity=true + UserName + Password (impersonation/explicit override)
|
||||
// The fourth combination — IntegratedSecurity=false with no UserName/Password — has
|
||||
// no way to authenticate against the /Hist-Integrated endpoint and is rejected.
|
||||
if (!options.IntegratedSecurity
|
||||
&& (!string.IsNullOrEmpty(options.UserName) || !string.IsNullOrEmpty(options.Password)))
|
||||
&& string.IsNullOrEmpty(options.UserName)
|
||||
&& string.IsNullOrEmpty(options.Password))
|
||||
{
|
||||
throw new ProtocolEvidenceMissingException("Open2 explicit username/password tag browse");
|
||||
throw new ProtocolEvidenceMissingException(
|
||||
"Tag browse / metadata requires either IntegratedSecurity=true OR an explicit UserName + Password.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -313,6 +313,55 @@ public sealed class HistorianClientIntegrationTests
|
||||
Assert.Equal(host, status.ServerName);
|
||||
}
|
||||
|
||||
// The validator inside HistorianWcfTagClient now allows IntegratedSecurity=false WHEN
|
||||
// explicit UserName + Password are provided (NTLM/Kerberos with non-current-user creds).
|
||||
// It still rejects the no-credentials-at-all case since there's no way to authenticate
|
||||
// against /Hist-Integrated.
|
||||
[Fact]
|
||||
public async Task GetTagMetadataAsync_NoAuthAndNoCredentials_Throws()
|
||||
{
|
||||
HistorianClient client = new(new HistorianClientOptions
|
||||
{
|
||||
Host = "localhost",
|
||||
IntegratedSecurity = false,
|
||||
UserName = string.Empty,
|
||||
Password = string.Empty,
|
||||
});
|
||||
await Assert.ThrowsAsync<ProtocolEvidenceMissingException>(
|
||||
() => client.GetTagMetadataAsync("anytag", CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetTagMetadataAsync_ExplicitCredentials_AgainstLocalHistorian()
|
||||
{
|
||||
// Live verification of the explicit-creds tag-metadata path. Gated on
|
||||
// HISTORIAN_USER + HISTORIAN_PASSWORD being set; skips cleanly otherwise. The path
|
||||
// routes through WCF Windows transport security with Credentials.Windows.ClientCredential.
|
||||
string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST");
|
||||
string? testTag = Environment.GetEnvironmentVariable("HISTORIAN_TEST_TAG");
|
||||
string? user = Environment.GetEnvironmentVariable("HISTORIAN_USER");
|
||||
string? password = Environment.GetEnvironmentVariable("HISTORIAN_PASSWORD");
|
||||
if (string.IsNullOrWhiteSpace(host) || string.IsNullOrWhiteSpace(testTag)
|
||||
|| string.IsNullOrWhiteSpace(user) || string.IsNullOrWhiteSpace(password)
|
||||
|| !OperatingSystem.IsWindows())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
HistorianClient client = new(new HistorianClientOptions
|
||||
{
|
||||
Host = host,
|
||||
IntegratedSecurity = false,
|
||||
UserName = user,
|
||||
Password = password,
|
||||
});
|
||||
|
||||
AVEVA.Historian.Client.Models.HistorianTagMetadata? metadata =
|
||||
await client.GetTagMetadataAsync(testTag, CancellationToken.None);
|
||||
Assert.NotNull(metadata);
|
||||
Assert.Equal(testTag, metadata.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetTagMetadataAsync_ReturnsConfiguredTestTagMetadata()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user