Portal Community

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