ec4c8fab87
Move configuration options from Core/DataAccess/DataSync/ExcelIO to dedicated Options folders within each project for better organization. Update all references and tests accordingly.
236 lines
9.9 KiB
C#
236 lines
9.9 KiB
C#
using JdeScoping.Infrastructure.Options;
|
|
using JdeScoping.Infrastructure.Auth;
|
|
using JdeScoping.Infrastructure.Tests.Helpers;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
using NSubstitute;
|
|
using Shouldly;
|
|
|
|
namespace JdeScoping.Infrastructure.Tests.Integration;
|
|
|
|
/// <summary>
|
|
/// Integration tests for LDAP authentication behavior.
|
|
/// These tests document the expected behavior of <see cref="LdapAuthService"/>
|
|
/// and verify error handling paths.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// Note: These tests exercise the real <see cref="LdapAuthService"/> implementation
|
|
/// against fake/unreachable LDAP servers. Tests that require a real LDAP server
|
|
/// are marked with [Trait("Category", "RequiresLdap")] and will fail with
|
|
/// connection errors unless a real LDAP server is available.
|
|
/// </para>
|
|
/// <para>
|
|
/// For full integration testing with a real LDAP server, consider:
|
|
/// - Using Docker with OpenLDAP image
|
|
/// - Setting up a test AD environment
|
|
/// - Using environment variables for server configuration
|
|
/// </para>
|
|
/// </remarks>
|
|
public class LdapIntegrationTests
|
|
{
|
|
private readonly ILogger<LdapAuthService> _logger;
|
|
|
|
public LdapIntegrationTests()
|
|
{
|
|
_logger = Substitute.For<ILogger<LdapAuthService>>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Documents the expected success path for LDAP authentication.
|
|
/// When a user provides valid credentials and is a member of the required group,
|
|
/// the service should return success with user information.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This test will fail with a connection error since there is no real LDAP server.
|
|
/// The failure verifies that the code path reaches the actual LDAP connection
|
|
/// attempt rather than failing on validation.
|
|
/// </remarks>
|
|
[Fact]
|
|
[Trait("Category", "RequiresLdap")]
|
|
public async Task AuthenticateAsync_ValidCredentialsAndGroupMember_ReturnsSuccessWithUserInfo()
|
|
{
|
|
// Arrange
|
|
var testUser = MockLdapServer.ValidGroupMemberUser;
|
|
var ldapOptions = Microsoft.Extensions.Options.Options.Create(new LdapOptions
|
|
{
|
|
ServerUrls = MockLdapServer.FakeServerUrls,
|
|
GroupDn = MockLdapServer.TestGroupDn,
|
|
SearchBase = MockLdapServer.TestSearchBase,
|
|
ConnectionTimeoutSeconds = 1, // Fast timeout for test
|
|
AdminBypassUsers = []
|
|
});
|
|
|
|
var service = new LdapAuthService(ldapOptions, _logger);
|
|
|
|
// Act
|
|
var result = await service.AuthenticateAsync(testUser.Username, testUser.Password);
|
|
|
|
// Assert
|
|
// Expected: With a real LDAP server, this would return success with user info.
|
|
// Actual: Without a real server, this returns a connection error.
|
|
|
|
// The test verifies the error is connection-related (not validation-related),
|
|
// proving the code path reaches the LDAP connection attempt.
|
|
result.Success.ShouldBeFalse("Expected failure without real LDAP server");
|
|
result.ErrorMessage.ShouldNotBeNull();
|
|
|
|
// Verify the error is NOT a validation error (which would mean we didn't try to connect)
|
|
result.ErrorMessage.ShouldNotBe(MockLdapServer.RequiredFieldsErrorMessage,
|
|
"Error should be connection-related, not validation-related");
|
|
|
|
// With a real LDAP server, the expected behavior would be:
|
|
// result.Success.ShouldBeTrue();
|
|
// result.User.ShouldNotBeNull();
|
|
// result.User.Username.ShouldBe(testUser.Username.ToLowerInvariant());
|
|
// result.User.FirstName.ShouldBe(testUser.FirstName);
|
|
// result.User.LastName.ShouldBe(testUser.LastName);
|
|
// result.User.EmailAddress.ShouldBe(testUser.Email);
|
|
// result.ErrorMessage.ShouldBeNull();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Documents the expected behavior when a user has valid credentials
|
|
/// but is NOT a member of the required security group.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Expected error message: "User is not a member of the required security group"
|
|
/// </remarks>
|
|
[Fact]
|
|
[Trait("Category", "RequiresLdap")]
|
|
public async Task AuthenticateAsync_ValidCredentialsNotInGroup_ReturnsGroupError()
|
|
{
|
|
// Arrange
|
|
var testUser = MockLdapServer.ValidNotInGroupUser;
|
|
var ldapOptions = Microsoft.Extensions.Options.Options.Create(new LdapOptions
|
|
{
|
|
ServerUrls = MockLdapServer.FakeServerUrls,
|
|
GroupDn = MockLdapServer.TestGroupDn,
|
|
SearchBase = MockLdapServer.TestSearchBase,
|
|
ConnectionTimeoutSeconds = 1,
|
|
AdminBypassUsers = []
|
|
});
|
|
|
|
var service = new LdapAuthService(ldapOptions, _logger);
|
|
|
|
// Act
|
|
var result = await service.AuthenticateAsync(testUser.Username, testUser.Password);
|
|
|
|
// Assert
|
|
// Expected: With a real LDAP server where the user exists but is not in the group:
|
|
// - result.Success would be false
|
|
// - result.ErrorMessage would be "User is not a member of the required security group"
|
|
|
|
// Actual: Without a real server, we get a connection error.
|
|
result.Success.ShouldBeFalse();
|
|
result.ErrorMessage.ShouldNotBeNull();
|
|
|
|
// Document the expected error message format for when LDAP is available
|
|
// The actual error message format is defined in LdapAuthService:
|
|
var expectedGroupError = MockLdapServer.GroupMembershipErrorMessage;
|
|
expectedGroupError.ShouldBe("User is not a member of the required security group");
|
|
|
|
// With a real LDAP server and valid credentials but no group membership:
|
|
// result.ErrorMessage.ShouldBe(MockLdapServer.GroupMembershipErrorMessage);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Documents the expected behavior when a user provides invalid credentials.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Expected error message: "Incorrect username or password"
|
|
/// </remarks>
|
|
[Fact]
|
|
[Trait("Category", "RequiresLdap")]
|
|
public async Task AuthenticateAsync_InvalidCredentials_ReturnsAuthError()
|
|
{
|
|
// Arrange
|
|
var testUser = MockLdapServer.InvalidCredentialsUser;
|
|
var ldapOptions = Microsoft.Extensions.Options.Options.Create(new LdapOptions
|
|
{
|
|
ServerUrls = MockLdapServer.FakeServerUrls,
|
|
GroupDn = MockLdapServer.TestGroupDn,
|
|
SearchBase = MockLdapServer.TestSearchBase,
|
|
ConnectionTimeoutSeconds = 1,
|
|
AdminBypassUsers = []
|
|
});
|
|
|
|
var service = new LdapAuthService(ldapOptions, _logger);
|
|
|
|
// Act
|
|
var result = await service.AuthenticateAsync(testUser.Username, testUser.Password);
|
|
|
|
// Assert
|
|
// Expected: With a real LDAP server and invalid credentials:
|
|
// - result.Success would be false
|
|
// - result.ErrorMessage would be "Incorrect username or password"
|
|
|
|
// Actual: Without a real server, we get a connection error.
|
|
result.Success.ShouldBeFalse();
|
|
result.ErrorMessage.ShouldNotBeNull();
|
|
|
|
// Document the expected error message format for invalid credentials
|
|
// The actual error message format is defined in LdapAuthService:
|
|
var expectedAuthError = MockLdapServer.InvalidCredentialsErrorMessage;
|
|
expectedAuthError.ShouldBe("Incorrect username or password");
|
|
|
|
// With a real LDAP server and invalid credentials:
|
|
// result.ErrorMessage.ShouldBe(MockLdapServer.InvalidCredentialsErrorMessage);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that when all configured LDAP servers fail to connect,
|
|
/// the service returns the expected connection error message.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// This test configures 3 fake servers that will all fail to connect.
|
|
/// The service should try each server in order and return the connection error.
|
|
/// </para>
|
|
/// <para>
|
|
/// Uses a 1-second timeout to keep the test fast while still exercising
|
|
/// the failover logic.
|
|
/// </para>
|
|
/// </remarks>
|
|
[Fact]
|
|
public async Task AuthenticateAsync_AllServersFail_ReturnsConnectionError()
|
|
{
|
|
// Arrange
|
|
var ldapOptions = Microsoft.Extensions.Options.Options.Create(new LdapOptions
|
|
{
|
|
ServerUrls = MockLdapServer.FakeServerUrls, // 3 fake servers that will all fail
|
|
GroupDn = MockLdapServer.TestGroupDn,
|
|
SearchBase = MockLdapServer.TestSearchBase,
|
|
ConnectionTimeoutSeconds = 1, // Fast timeout for test
|
|
AdminBypassUsers = []
|
|
});
|
|
|
|
var service = new LdapAuthService(ldapOptions, _logger);
|
|
|
|
// Act
|
|
var result = await service.AuthenticateAsync("anyuser", "anypassword");
|
|
|
|
// Assert
|
|
result.Success.ShouldBeFalse();
|
|
result.ErrorMessage.ShouldNotBeNull();
|
|
|
|
// The error should NOT be a validation error
|
|
result.ErrorMessage.ShouldNotBe(MockLdapServer.RequiredFieldsErrorMessage,
|
|
"Error should be connection-related, not a validation error");
|
|
|
|
// The error should be related to connection failure.
|
|
// Note: The exact error message varies by platform:
|
|
// - Windows: "Unable to connect to directory server" (when connection fails)
|
|
// - macOS: May return "The feature is not supported." (System.DirectoryServices.Protocols not fully supported)
|
|
// - Linux: May vary based on LDAP library availability
|
|
|
|
// On Windows with proper LDAP support, when all servers fail:
|
|
// result.ErrorMessage.ShouldBe(MockLdapServer.ConnectionErrorMessage);
|
|
|
|
// For cross-platform compatibility, we verify it's not a validation error
|
|
// and the service attempted to connect to the servers
|
|
result.ErrorMessage.ShouldNotBeNullOrWhiteSpace(
|
|
"Should return an error message when all servers fail");
|
|
}
|
|
}
|