Passport
Testing Custom IAM
Unit test patterns for custom permission resolvers and role providers — mock the user context, inject test data, and assert the expected Allow/Deny/Defer outcome.
Unit Testing a Permission Resolver
using Xunit;
using NSubstitute;
using BizFirst.Essentials.Passport.IAM;
public class DepartmentPayrollResolverTests
{
private readonly IUserProfileService _profiles = Substitute.For<IUserProfileService>();
private readonly DepartmentBasedPayrollResolver _sut;
public DepartmentPayrollResolverTests()
{
_sut = new DepartmentBasedPayrollResolver(_profiles);
}
[Fact]
public async Task Finance_Employee_Can_Access_Payroll()
{
// Arrange
_profiles.GetAsync("user-1", Arg.Any<CancellationToken>())
.Returns(new UserProfile { Department = "Finance", EmployeeType = "employee" });
var context = BuildContext(userId: "user-1", resourceType: "payroll",
permission: "workflow.initiate");
// Act
var result = await _sut.ResolveAsync(context, CancellationToken.None);
// Assert
Assert.Equal(PermissionResolution.Allow, result);
}
[Fact]
public async Task Contractor_Cannot_Access_Payroll()
{
// Arrange
_profiles.GetAsync("user-2", Arg.Any<CancellationToken>())
.Returns(new UserProfile { Department = "Finance", EmployeeType = "contractor" });
var context = BuildContext(userId: "user-2", resourceType: "payroll",
permission: "workflow.initiate");
// Act
var result = await _sut.ResolveAsync(context, CancellationToken.None);
// Assert
Assert.Equal(PermissionResolution.Deny, result);
}
[Fact]
public async Task Non_Payroll_Resource_Returns_Defer()
{
// Arrange — no profile needed; should defer immediately
var context = BuildContext(userId: "user-1", resourceType: "form",
permission: "form.submit");
// Act
var result = await _sut.ResolveAsync(context, CancellationToken.None);
// Assert — resolver doesn't apply to non-payroll resources
Assert.Equal(PermissionResolution.Defer, result);
await _profiles.DidNotReceive().GetAsync(Arg.Any<string>(), Arg.Any<CancellationToken>());
}
private static PermissionContext BuildContext(
string userId, string resourceType, string permission,
bool baseAllowed = true)
{
return new PermissionContext
{
User = new IDInfo
{
UserId = userId,
TenantId = "tenant-abc",
Email = $"{userId}@example.com",
Roles = ["manager"]
},
Permission = permission,
ResourceType = resourceType,
BaseAllowed = baseAllowed,
Environment = new EnvironmentContext
{
Now = new DateTimeOffset(2026, 5, 25, 10, 0, 0, TimeSpan.Zero),
IsMfaVerified= true
}
};
}
}
Unit Testing a Role Provider
public class ProjectRoleProviderTests
{
private readonly IProjectRepository _projects = Substitute.For<IProjectRepository>();
private readonly IMemoryCache _cache = new MemoryCache(new MemoryCacheOptions());
private readonly ProjectRoleProvider _sut;
public ProjectRoleProviderTests()
{
_sut = new ProjectRoleProvider(_projects, _cache, NullLogger<ProjectRoleProvider>.Instance);
}
[Fact]
public async Task Active_Project_Manager_Gets_ProjectManager_Role()
{
_projects.GetUserAssignmentsAsync("user-1", "tenant-abc", Arg.Any<CancellationToken>())
.Returns([new ProjectAssignment { Role = "ProjectManager", IsActive = true }]);
var roles = await _sut.GetRolesAsync("user-1", "tenant-abc", CancellationToken.None);
Assert.Contains("project-manager", roles);
Assert.Contains("project-member", roles);
}
[Fact]
public async Task No_Active_Assignments_Returns_Empty()
{
_projects.GetUserAssignmentsAsync("user-2", "tenant-abc", Arg.Any<CancellationToken>())
.Returns([]);
var roles = await _sut.GetRolesAsync("user-2", "tenant-abc", CancellationToken.None);
Assert.Empty(roles);
}
[Fact]
public async Task External_Service_Failure_Returns_Empty_Not_Exception()
{
_projects.GetUserAssignmentsAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
.ThrowsAsync(new HttpRequestException("Service unavailable"));
// Should not throw — returns empty list on failure
var roles = await _sut.GetRolesAsync("user-3", "tenant-abc", CancellationToken.None);
Assert.Empty(roles);
}
}
Integration Test with Full Pipeline
// Integration test — uses the full evaluation pipeline
public class CustomIAMIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
{
[Fact]
public async Task Finance_Contractor_Cannot_Access_Payroll_Endpoint()
{
// Arrange — create a JWT token for a contractor in Finance
var token = TestJwtBuilder.Create()
.WithUserId("contractor-1")
.WithRole("manager")
.WithCustomAttribute("department", "Finance")
.WithCustomAttribute("employeeType", "contractor")
.Build();
var client = _factory.CreateClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", token);
// Act
var response = await client.PostAsync("/api/workflows/payroll-workflow/execute", null);
// Assert — 403 Forbidden even though role is "manager"
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
}
}