Initial commit: scadaproj umbrella — sister-project index, auth component normalization (design + GAPS), and the built ZB.MOM.WW.Auth shared library (0.1.0, flattened in).
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
using ZB.MOM.WW.Auth.Abstractions.Ldap;
|
||||
using ZB.MOM.WW.Auth.Ldap;
|
||||
|
||||
namespace ZB.MOM.WW.Auth.Ldap.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Task 6 failure-mode tests. These pin the fail-closed contract: every error path returns a
|
||||
/// structured <see cref="LdapAuthResult.Fail(LdapAuthFailure)"/>, the method never throws, and
|
||||
/// a successful result always carries at least one group.
|
||||
/// </summary>
|
||||
public class LdapAuthServiceFailureTests
|
||||
{
|
||||
// Mirrors the happy-path test defaults (insecure plaintext dev transport, service account
|
||||
// set, DisplayNameAttribute aligned with the fake's "displayName" key).
|
||||
private static LdapOptions Opts() => new()
|
||||
{
|
||||
Enabled = true,
|
||||
Server = "x",
|
||||
Port = 3893,
|
||||
Transport = LdapTransport.None,
|
||||
AllowInsecure = true,
|
||||
SearchBase = "dc=x",
|
||||
ServiceAccountDn = "cn=svc,dc=x",
|
||||
ServiceAccountPassword = "svcpw",
|
||||
UserNameAttribute = "cn",
|
||||
DisplayNameAttribute = "displayName",
|
||||
GroupAttribute = "memberOf",
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public async Task BadCredentials_WhenUserBindThrows()
|
||||
{
|
||||
var fake = new FakeLdapConnection()
|
||||
.WithUserEntry("cn=alice,dc=x", new[] { "cn=Eng,dc=x" })
|
||||
.ThrowOnUserBind();
|
||||
|
||||
var r = await new LdapAuthService(Opts(), new FakeLdapConnectionFactory(fake))
|
||||
.AuthenticateAsync("alice", "bad", default);
|
||||
|
||||
Assert.False(r.Succeeded);
|
||||
Assert.Equal(LdapAuthFailure.BadCredentials, r.Failure);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UserNotFound_WhenZeroMatches()
|
||||
{
|
||||
var fake = new FakeLdapConnection().WithNoMatch();
|
||||
|
||||
var r = await new LdapAuthService(Opts(), new FakeLdapConnectionFactory(fake))
|
||||
.AuthenticateAsync("ghost", "pw", default);
|
||||
|
||||
Assert.False(r.Succeeded);
|
||||
Assert.Equal(LdapAuthFailure.UserNotFound, r.Failure);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AmbiguousUser_WhenMultipleMatches()
|
||||
{
|
||||
var fake = new FakeLdapConnection().WithDuplicateMatch();
|
||||
|
||||
var r = await new LdapAuthService(Opts(), new FakeLdapConnectionFactory(fake))
|
||||
.AuthenticateAsync("alice", "pw", default);
|
||||
|
||||
Assert.False(r.Succeeded);
|
||||
Assert.Equal(LdapAuthFailure.AmbiguousUser, r.Failure);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AmbiguousUser_DoesNotAttemptUserBind()
|
||||
{
|
||||
var fake = new FakeLdapConnection().WithDuplicateMatch();
|
||||
|
||||
await new LdapAuthService(Opts(), new FakeLdapConnectionFactory(fake))
|
||||
.AuthenticateAsync("alice", "pw", default);
|
||||
|
||||
// Only the service-account bind should have happened; never bind an ambiguous DN.
|
||||
Assert.Equal(new[] { "cn=svc,dc=x" }, fake.BoundDns);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GroupLookupFailed_WhenUserHasNoGroups()
|
||||
{
|
||||
var fake = new FakeLdapConnection()
|
||||
.WithUserEntry("cn=alice,dc=x", memberOf: Array.Empty<string>());
|
||||
|
||||
var r = await new LdapAuthService(Opts(), new FakeLdapConnectionFactory(fake))
|
||||
.AuthenticateAsync("alice", "pw", default);
|
||||
|
||||
Assert.False(r.Succeeded);
|
||||
Assert.Equal(LdapAuthFailure.GroupLookupFailed, r.Failure);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ServiceAccountBindFailed_Distinctly_WhenServiceBindThrows()
|
||||
{
|
||||
var fake = new FakeLdapConnection()
|
||||
.WithUserEntry("cn=alice,dc=x", new[] { "cn=Eng,dc=x" })
|
||||
.ThrowOnServiceBind();
|
||||
|
||||
var r = await new LdapAuthService(Opts(), new FakeLdapConnectionFactory(fake))
|
||||
.AuthenticateAsync("alice", "pw", default);
|
||||
|
||||
Assert.False(r.Succeeded);
|
||||
Assert.Equal(LdapAuthFailure.ServiceAccountBindFailed, r.Failure);
|
||||
// Distinct from BadCredentials: a service-account problem is a system misconfiguration,
|
||||
// not the end user's fault.
|
||||
Assert.NotEqual(LdapAuthFailure.BadCredentials, r.Failure);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
public async Task BadCredentials_WhenUsernameNullOrWhitespace_NoConnectionAttempted(string? username)
|
||||
{
|
||||
// I4: an empty/whitespace/null username is rejected up front as BadCredentials,
|
||||
// before any connection or bind is attempted (and a null can't NRE into the catch-all).
|
||||
var fake = new FakeLdapConnection().WithUserEntry("cn=alice,dc=x", new[] { "cn=Eng,dc=x" });
|
||||
|
||||
var r = await new LdapAuthService(Opts(), new FakeLdapConnectionFactory(fake))
|
||||
.AuthenticateAsync(username!, "pw", default);
|
||||
|
||||
Assert.False(r.Succeeded);
|
||||
Assert.Equal(LdapAuthFailure.BadCredentials, r.Failure);
|
||||
Assert.Null(fake.ConnectArgs); // never connected
|
||||
Assert.Empty(fake.BoundDns); // never bound
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Throws_WhenCancellationRequested()
|
||||
{
|
||||
// I3: a pre-cancelled token is observed at entry, before any work.
|
||||
var fake = new FakeLdapConnection().WithUserEntry("cn=alice,dc=x", new[] { "cn=Eng,dc=x" });
|
||||
var svc = new LdapAuthService(Opts(), new FakeLdapConnectionFactory(fake));
|
||||
|
||||
await Assert.ThrowsAnyAsync<OperationCanceledException>(
|
||||
() => svc.AuthenticateAsync("alice", "pw", new CancellationToken(canceled: true)));
|
||||
|
||||
Assert.Null(fake.ConnectArgs); // never connected
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NeverThrows_OnConnectFailure()
|
||||
{
|
||||
var fake = new FakeLdapConnection()
|
||||
.WithUserEntry("cn=alice,dc=x", new[] { "cn=Eng,dc=x" })
|
||||
.ThrowOnConnect();
|
||||
|
||||
var r = await new LdapAuthService(Opts(), new FakeLdapConnectionFactory(fake))
|
||||
.AuthenticateAsync("alice", "pw", default);
|
||||
|
||||
Assert.False(r.Succeeded);
|
||||
// Directory unreachable is a system-side failure -> bucketed under ServiceAccountBindFailed.
|
||||
Assert.Equal(LdapAuthFailure.ServiceAccountBindFailed, r.Failure);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user