using Bit.Core.Tools.Models.Data; using Bit.Identity.IdentityServer.RequestValidators.SendAccess; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Duende.IdentityModel; using Duende.IdentityServer.Validation; using Xunit; namespace Bit.Identity.Test.IdentityServer.SendAccess; [SutProviderCustomize] public class SendNeverAuthenticateRequestValidatorTests { /// /// To support the static hashing function theses GUIDs and Key must be hardcoded /// private static readonly string _testHashKey = "test-key-123456789012345678901234567890"; // These Guids are static and ensure the correct index for each error type private static readonly Guid _invalidSendGuid = Guid.Parse("1b35fbf3-a14a-4d48-82b7-2adc34fdae6f"); private static readonly Guid _emailSendGuid = Guid.Parse("bc8e2ef5-a0bd-44d2-bdb7-5902be6f5c41"); private static readonly Guid _passwordSendGuid = Guid.Parse("da36fa27-f0e8-4701-a585-d3d8c2f67c4b"); private static readonly NeverAuthenticate _authMethod = new(); [Theory, BitAutoData] public async Task ValidateRequestAsync_GuidErrorSelected_ReturnsInvalidSendId( SutProvider sutProvider, [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest) { // Arrange tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(_invalidSendGuid); var context = new ExtensionGrantValidationContext { Request = tokenRequest }; sutProvider.GetDependency().SendDefaultHashKey = _testHashKey; // Act var result = await sutProvider.Sut.ValidateRequestAsync(context, _authMethod, _invalidSendGuid); // Assert Assert.True(result.IsError); Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, result.Error); Assert.Equal(SendAccessConstants.SendIdGuidValidatorResults.InvalidSendId, result.ErrorDescription); var customResponse = result.CustomResponse as Dictionary; Assert.NotNull(customResponse); Assert.Equal( SendAccessConstants.SendIdGuidValidatorResults.InvalidSendId, customResponse[SendAccessConstants.SendAccessError]); } [Theory, BitAutoData] public async Task ValidateRequestAsync_EmailErrorSelected_HasEmail_ReturnsEmailInvalid( SutProvider sutProvider, [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, string email) { // Arrange tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(_emailSendGuid, sendEmail: email); var context = new ExtensionGrantValidationContext { Request = tokenRequest }; sutProvider.GetDependency().SendDefaultHashKey = _testHashKey; // Act var result = await sutProvider.Sut.ValidateRequestAsync(context, _authMethod, _emailSendGuid); // Assert Assert.True(result.IsError); Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, result.Error); Assert.Equal(SendAccessConstants.EmailOtpValidatorResults.EmailInvalid, result.ErrorDescription); var customResponse = result.CustomResponse as Dictionary; Assert.NotNull(customResponse); Assert.Equal(SendAccessConstants.EmailOtpValidatorResults.EmailInvalid, customResponse[SendAccessConstants.SendAccessError]); } [Theory, BitAutoData] public async Task ValidateRequestAsync_EmailErrorSelected_NoEmail_ReturnsEmailRequired( SutProvider sutProvider, [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest) { // Arrange tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(_emailSendGuid); var context = new ExtensionGrantValidationContext { Request = tokenRequest }; sutProvider.GetDependency().SendDefaultHashKey = _testHashKey; // Act var result = await sutProvider.Sut.ValidateRequestAsync(context, _authMethod, _emailSendGuid); // Assert Assert.True(result.IsError); Assert.Equal(OidcConstants.TokenErrors.InvalidRequest, result.Error); Assert.Equal(SendAccessConstants.EmailOtpValidatorResults.EmailRequired, result.ErrorDescription); var customResponse = result.CustomResponse as Dictionary; Assert.NotNull(customResponse); Assert.Equal(SendAccessConstants.EmailOtpValidatorResults.EmailRequired, customResponse[SendAccessConstants.SendAccessError]); } [Theory, BitAutoData] public async Task ValidateRequestAsync_PasswordErrorSelected_HasPassword_ReturnsPasswordDoesNotMatch( SutProvider sutProvider, [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, string password) { // Arrange tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(_passwordSendGuid, passwordHash: password); var context = new ExtensionGrantValidationContext { Request = tokenRequest }; sutProvider.GetDependency().SendDefaultHashKey = _testHashKey; // Act var result = await sutProvider.Sut.ValidateRequestAsync(context, _authMethod, _passwordSendGuid); // Assert Assert.True(result.IsError); Assert.Equal(OidcConstants.TokenErrors.InvalidRequest, result.Error); Assert.Equal(SendAccessConstants.PasswordValidatorResults.RequestPasswordDoesNotMatch, result.ErrorDescription); var customResponse = result.CustomResponse as Dictionary; Assert.NotNull(customResponse); Assert.Equal(SendAccessConstants.PasswordValidatorResults.RequestPasswordDoesNotMatch, customResponse[SendAccessConstants.SendAccessError]); } [Theory, BitAutoData] public async Task ValidateRequestAsync_PasswordErrorSelected_NoPassword_ReturnsPasswordRequired( SutProvider sutProvider, [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest) { // Arrange tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(_passwordSendGuid); var context = new ExtensionGrantValidationContext { Request = tokenRequest }; sutProvider.GetDependency().SendDefaultHashKey = _testHashKey; // Act var result = await sutProvider.Sut.ValidateRequestAsync(context, _authMethod, _passwordSendGuid); // Assert Assert.True(result.IsError); Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, result.Error); Assert.Equal(SendAccessConstants.PasswordValidatorResults.RequestPasswordIsRequired, result.ErrorDescription); var customResponse = result.CustomResponse as Dictionary; Assert.NotNull(customResponse); Assert.Equal(SendAccessConstants.PasswordValidatorResults.RequestPasswordIsRequired, customResponse[SendAccessConstants.SendAccessError]); } [Theory, BitAutoData] public async Task ValidateRequestAsync_NullHashKey_UsesEmptyKey( SutProvider sutProvider, [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest) { // Arrange tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(_invalidSendGuid); var context = new ExtensionGrantValidationContext { Request = tokenRequest }; sutProvider.GetDependency().SendDefaultHashKey = null; // Act var result = await sutProvider.Sut.ValidateRequestAsync(context, _authMethod, _invalidSendGuid); // Assert Assert.True(result.IsError); Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, result.Error); Assert.Contains(result.ErrorDescription, SendAccessConstants.SendIdGuidValidatorResults.InvalidSendId); } [Theory, BitAutoData] public async Task ValidateRequestAsync_EmptyHashKey_UsesEmptyKey( SutProvider sutProvider, [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest) { // Arrange tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(_invalidSendGuid); var context = new ExtensionGrantValidationContext { Request = tokenRequest }; sutProvider.GetDependency().SendDefaultHashKey = ""; // Act var result = await sutProvider.Sut.ValidateRequestAsync(context, _authMethod, _invalidSendGuid); // Assert Assert.True(result.IsError); Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, result.Error); Assert.Contains(result.ErrorDescription, SendAccessConstants.SendIdGuidValidatorResults.InvalidSendId); } [Theory, BitAutoData] public async Task ValidateRequestAsync_ConsistentBehavior_SameSendIdSameResult( SutProvider sutProvider, [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, Guid sendId) { // Arrange tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId); var context = new ExtensionGrantValidationContext { Request = tokenRequest }; sutProvider.GetDependency().SendDefaultHashKey = "consistent-test-key-123456789012345678901234567890"; // Act var result1 = await sutProvider.Sut.ValidateRequestAsync(context, _authMethod, sendId); var result2 = await sutProvider.Sut.ValidateRequestAsync(context, _authMethod, sendId); // Assert Assert.Equal(result1.ErrorDescription, result2.ErrorDescription); Assert.Equal(result1.Error, result2.Error); var customResponse1 = result1.CustomResponse as Dictionary; var customResponse2 = result2.CustomResponse as Dictionary; Assert.Equal(customResponse1[SendAccessConstants.SendAccessError], customResponse2[SendAccessConstants.SendAccessError]); } [Theory, BitAutoData] public async Task ValidateRequestAsync_DifferentSendIds_CanReturnDifferentResults( SutProvider sutProvider, [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, Guid sendId1, Guid sendId2) { // Arrange tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId1); var context = new ExtensionGrantValidationContext { Request = tokenRequest }; sutProvider.GetDependency().SendDefaultHashKey = "different-test-key-123456789012345678901234567890"; // Act var result1 = await sutProvider.Sut.ValidateRequestAsync(context, _authMethod, sendId1); var result2 = await sutProvider.Sut.ValidateRequestAsync(context, _authMethod, sendId2); // Assert - Both should be errors Assert.True(result1.IsError); Assert.True(result2.IsError); // Both should have valid error types var validErrors = new[] { SendAccessConstants.SendIdGuidValidatorResults.InvalidSendId, SendAccessConstants.EmailOtpValidatorResults.EmailRequired, SendAccessConstants.PasswordValidatorResults.RequestPasswordIsRequired }; Assert.Contains(result1.ErrorDescription, validErrors); Assert.Contains(result2.ErrorDescription, validErrors); } [Fact] public void Constructor_WithValidGlobalSettings_CreatesInstance() { // Arrange var globalSettings = new Core.Settings.GlobalSettings { SendDefaultHashKey = "test-key-123456789012345678901234567890" }; // Act var validator = new SendNeverAuthenticateRequestValidator(globalSettings); // Assert Assert.NotNull(validator); } }