using DodoSSH.Crypto;
namespace DodoSSH.Contracts.Tests;
///
/// Guards the two key statement types against drifting apart.
///
///
///
/// is the wire DTO; is what the
/// canonical encoding in docs/crypto.md §7.1 is defined over. They are separate on purpose — one
/// may gain JSON fields freely, the other cannot change without invalidating every stored binding,
/// and DodoSSH.Crypto must not depend on the contract assembly.
///
///
/// The hazard that separation creates is a field added to the DTO and silently left out of the
/// hash. A client would then sign and bind a statement that omits it, and the field would be
/// unauthenticated data the server could change at will. This test makes that a build failure:
/// adding a field to the DTO forces a deliberate decision about whether it is covered, and if it
/// is, a spec version bump.
///
///
public sealed class KeyStatementDriftTests
{
[Fact]
public void BothTypes_DeclareTheSameFields()
{
var contract = PropertyNames();
var canonical = PropertyNames();
canonical.ShouldBe(
contract,
"KeyStatement and KeyStatementFields disagree. A field on the wire that the canonical "
+ "encoding does not cover is unauthenticated: the identity provider never signs over "
+ "it, so the server can change it undetected. Cover it and bump the statement version, "
+ "or document why it is deliberately outside the binding.");
}
[Fact]
public void BothTypes_AgreeOnFieldTypes()
{
// Names alone would not catch a string becoming a Uri, or an int becoming a long — either
// of which changes what gets encoded without changing what gets listed.
var contract = PropertyTypes();
var canonical = PropertyTypes();
canonical.ShouldBe(contract);
}
private static IReadOnlyList PropertyNames() =>
[.. typeof(T)
.GetProperties()
.Select(p => p.Name)
.Where(name => !string.Equals(name, "EqualityContract", StringComparison.Ordinal))
.Order(StringComparer.Ordinal)];
private static IReadOnlyList PropertyTypes() =>
[.. typeof(T)
.GetProperties()
.Where(p => !string.Equals(p.Name, "EqualityContract", StringComparison.Ordinal))
.Select(p => $"{p.Name}:{p.PropertyType.Name}")
.Order(StringComparer.Ordinal)];
}