using DXPBLL.Auth; using Xunit; namespace DXPTests; /// /// Covers DXPBLL.Auth.DXPPasswordHasher — PBKDF2-SHA256, 210k iterations, 16-byte random salt, /// self-describing "{iterations}.{salt}.{hash}" stored format. This class is deliberately left /// as-is (not migrated onto GB5Shared.EncryptionHelper.PasswordHasher, which mirrors this exact /// algorithm) — out of scope for the JWT/refresh-token consolidation pass this test project was /// added for. These tests exist purely so DXP's own copy has the same coverage Entitlement's /// shared PasswordHasher already has, not because this pass changes it. /// public class DXPPasswordHasherTests { [Fact] public void Test_Hash_ThenVerify_SamePassword_RoundTrips() { var stored = DXPPasswordHasher.Hash("Correct-Horse-Battery-Staple"); Assert.True(DXPPasswordHasher.Verify("Correct-Horse-Battery-Staple", stored)); } [Fact] public void Test_Verify_WrongPassword_IsRejected() { var stored = DXPPasswordHasher.Hash("Correct-Horse-Battery-Staple"); Assert.False(DXPPasswordHasher.Verify("wrong-password", stored)); } [Fact] public void Test_Hash_IsSelfDescribing_StoresIterationCountAndIsNotPlaintext() { var stored = DXPPasswordHasher.Hash("some-password"); var parts = stored.Split('.', 3); Assert.Equal(3, parts.Length); Assert.Equal("210000", parts[0]); Assert.DoesNotContain("some-password", stored); } [Fact] public void Test_Hash_TwoCallsSamePassword_ProduceDifferentHashes_DueToRandomSalt() { var first = DXPPasswordHasher.Hash("same-password"); var second = DXPPasswordHasher.Hash("same-password"); Assert.NotEqual(first, second); Assert.True(DXPPasswordHasher.Verify("same-password", first)); Assert.True(DXPPasswordHasher.Verify("same-password", second)); } [Theory] [InlineData("")] [InlineData("not-the-right-format")] [InlineData("210000.onlyoneseparator")] public void Test_Verify_MalformedStoredHash_WrongPartCount_ReturnsFalse_DoesNotThrow(string malformed) { Assert.False(DXPPasswordHasher.Verify("any-password", malformed)); } [Fact] public void Test_Verify_ThreePartsButNonNumericIterationCount_Throws_CurrentBehavior() { // Characterization, not a desired behavior: DXPPasswordHasher.Verify uses int.Parse (not // int.TryParse) on the iteration-count segment, so a 3-part string with a non-numeric // first segment throws FormatException instead of returning false. The shared // GB5Shared.EncryptionHelper.PasswordHasher.Verify (used by Entitlement) does not have // this gap — it uses int.TryParse and a try/catch around the base64 decode. Confirmed // real via direct comparison; DXPPasswordHasher is explicitly out of scope for this pass // (see this file's header comment), so this is documented here, not fixed. Assert.Throws(() => DXPPasswordHasher.Verify("any-password", "notanumber.c2FsdA==.aGFzaA==")); } }