test(template): M2.8 review nits — stale-binding comment + stale-ID & inert-check tests (#23)

Add code comments in ValidateConnectionBindingCompleteness explaining
that the unbound-attribute branch also covers the silently-dropped
stale-binding case (cross-reference FlatteningService.ApplyConnectionBindings),
and that the `continue` skips the exists-at-site check for unbound attrs.

Add two new tests:
- FlatteningPipelineConnectionBindingTests: stale DataConnectionId (999)
  not present in site connections → flattener drops it silently →
  validator reports ConnectionBinding Error, IsValid false.
- ValidationServiceTests: enforce:true + siteConnectionNames:null on a
  properly-bound attribute → no ConnectionBinding error (exists-at-site
  check stays inert when site set is not supplied).
This commit is contained in:
Joseph Doherty
2026-06-16 05:34:56 -04:00
parent 3b79b896cf
commit 21b801b71f
3 changed files with 65 additions and 0 deletions
@@ -96,4 +96,27 @@ public class FlatteningPipelineConnectionBindingTests
Assert.DoesNotContain(result.Value.Validation.Errors,
e => e.Category == ValidationCategory.ConnectionBinding);
}
[Fact]
public async Task FlattenAndValidate_BindingToStaleDeletedConnection_ReportsBindingError()
{
// M2.8 (#23): FlatteningService.ApplyConnectionBindings silently drops a
// binding whose DataConnectionId doesn't resolve to any loaded site
// DataConnection (stale / deleted connection). The flattener leaves
// BoundDataConnectionId == null, so the validator treats the attribute as
// unbound and gates the deployment with a ConnectionBinding Error.
//
// Arrange: the instance binding points at id 999, but the site only has
// the connection with id=ConnectionId (7). The flattener can't resolve 999
// and drops the binding silently; the validator then flags it.
const int StaleConnectionId = 999;
Arrange(boundConnectionId: StaleConnectionId);
var result = await _sut.FlattenAndValidateAsync(InstanceId);
Assert.True(result.IsSuccess);
Assert.False(result.Value.Validation.IsValid);
Assert.Contains(result.Value.Validation.Errors,
e => e.Category == ValidationCategory.ConnectionBinding);
}
}
@@ -298,6 +298,36 @@ public class ValidationServiceTests
Assert.False(result.IsValid);
}
[Fact]
public void Validate_BoundAttributeWithNoSiteSet_DeployTime_ExistsAtSiteCheckIsInert()
{
// M2.8 (#23): when siteConnectionNames is null the "exists at site" half of the
// binding check stays inert — a properly-bound data-sourced attribute must NOT
// produce a ConnectionBinding error, even under deploy-time enforcement.
// This pins the contract: passing enforce:true + siteConnectionNames:null is safe
// (e.g. when the caller doesn't have a site connection set available yet).
var config = new FlattenedConfiguration
{
InstanceUniqueName = "Instance1",
Attributes =
[
new ResolvedAttribute
{
CanonicalName = "Temp",
DataType = "Double",
DataSourceReference = "ns=2;s=Temp",
BoundDataConnectionId = 7,
BoundDataConnectionName = "PlantBus"
}
]
};
var result = _sut.Validate(config, enforceConnectionBindings: true, siteConnectionNames: null);
Assert.DoesNotContain(result.Errors, e => e.Category == ValidationCategory.ConnectionBinding);
Assert.True(result.IsValid);
}
[Fact]
public void Validate_EmptyConfig_ReturnsWarning()
{