Files
Joseph Doherty ec4c8fab87 refactor: relocate options classes to dedicated Options folders
Move configuration options from Core/DataAccess/DataSync/ExcelIO to
dedicated Options folders within each project for better organization.
Update all references and tests accordingly.
2026-01-03 08:55:08 -05:00

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