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)]; }