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=="));
}
}