Files
jobtrackingapp/JobTrackerApi.Tests/ProEntitlementAuthorizationTests.cs
T
2026-08-09 12:51:46 +02:00

123 lines
5.5 KiB
C#

using System.Security.Claims;
using JobTrackerApi.Controllers;
using JobTrackerApi.Models;
using JobTrackerApi.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Policy;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Moq;
using Xunit;
namespace JobTrackerApi.Tests;
public sealed class ProEntitlementAuthorizationTests
{
[Theory]
[InlineData("Premium")]
[InlineData("Admin")]
public async Task Current_pro_or_admin_role_satisfies_policy(string role)
{
var (handler, user) = Handler(role);
var context = Context(user.Id, includeStalePremiumClaim: false);
await handler.HandleAsync(context);
Assert.True(context.HasSucceeded);
}
[Fact]
public async Task Free_user_fails_even_when_session_contains_a_stale_premium_claim()
{
var (handler, user) = Handler();
var context = Context(user.Id, includeStalePremiumClaim: true);
await handler.HandleAsync(context);
Assert.False(context.HasSucceeded);
}
[Fact]
public async Task Pro_user_with_ai_disabled_fails_with_the_privacy_code()
{
var (handler, user) = Handler("Premium");
user.AiEnabled = false;
var context = Context(user.Id, includeStalePremiumClaim: false);
await handler.HandleAsync(context);
Assert.False(context.HasSucceeded);
Assert.Contains(context.FailureReasons, reason => reason.Message == ProEntitlement.DisabledCode);
}
[Fact]
public async Task Policy_failure_returns_the_stable_pro_required_contract()
{
var http = new DefaultHttpContext();
http.Response.Body = new MemoryStream();
var requirement = new ProEntitlementRequirement();
var failure = AuthorizationFailure.Failed(new[] { requirement });
var result = PolicyAuthorizationResult.Forbid(failure);
await new ProEntitlementAuthorizationResultHandler().HandleAsync(
_ => Task.CompletedTask,
http,
new AuthorizationPolicy(new[] { requirement }, Array.Empty<string>()),
result);
http.Response.Body.Position = 0;
var body = await new StreamReader(http.Response.Body).ReadToEndAsync();
Assert.Equal(StatusCodes.Status403Forbidden, http.Response.StatusCode);
Assert.Contains("\"code\":\"pro_required\"", body);
Assert.DoesNotContain("Premium", body, StringComparison.OrdinalIgnoreCase);
}
[Theory]
[InlineData(typeof(AiWorkspaceController), nameof(AiWorkspaceController.Generate))]
[InlineData(typeof(CvVariantController), nameof(CvVariantController.AiAssist))]
[InlineData(typeof(ProfileCvController), nameof(ProfileCvController.Upload))]
[InlineData(typeof(ProfileCvController), nameof(ProfileCvController.Reprocess))]
[InlineData(typeof(ProfileCvController), nameof(ProfileCvController.Rebuild))]
[InlineData(typeof(ProfileCvController), nameof(ProfileCvController.RewriteSection))]
[InlineData(typeof(ProfileCvController), nameof(ProfileCvController.BuildRewritePreview))]
[InlineData(typeof(ProfileCvController), nameof(ProfileCvController.ExportProfileCvPdf))]
[InlineData(typeof(ProfileCvController), nameof(ProfileCvController.Parse))]
[InlineData(typeof(ProfileCvController), nameof(ProfileCvController.Improve))]
[InlineData(typeof(JobApplicationsController), nameof(JobApplicationsController.RefreshAi))]
[InlineData(typeof(JobApplicationsController), nameof(JobApplicationsController.GetCandidateFit))]
[InlineData(typeof(StrategySnapshotController), nameof(StrategySnapshotController.Enqueue))]
[InlineData(typeof(JobApplicationsController), nameof(JobApplicationsController.GetInterviewPrep))]
[InlineData(typeof(JobApplicationsController), nameof(JobApplicationsController.GenerateTailoredCvDraft))]
[InlineData(typeof(JobApplicationsController), nameof(JobApplicationsController.GenerateApplicationPackage))]
[InlineData(typeof(JobApplicationsController), nameof(JobApplicationsController.GetFollowUpDraft))]
public void Explicit_ai_action_requires_the_pro_policy(Type controller, string action)
{
var method = controller.GetMethod(action);
Assert.NotNull(method);
var attributes = method!.GetCustomAttributes(typeof(AuthorizeAttribute), true).Cast<AuthorizeAttribute>()
.Concat(controller.GetCustomAttributes(typeof(AuthorizeAttribute), true).Cast<AuthorizeAttribute>());
Assert.Contains(attributes,
attribute => attribute.Policy == ProEntitlement.Policy);
}
private static (ProEntitlementHandler Handler, ApplicationUser User) Handler(params string[] roles)
{
var user = new ApplicationUser { Id = "user-1" };
var users = new Mock<UserManager<ApplicationUser>>(
Mock.Of<IUserStore<ApplicationUser>>(), null!, null!, null!, null!, null!, null!, null!, null!);
users.Setup(x => x.FindByIdAsync(user.Id)).ReturnsAsync(user);
users.Setup(x => x.GetRolesAsync(user)).ReturnsAsync(roles);
return (new ProEntitlementHandler(users.Object), user);
}
private static AuthorizationHandlerContext Context(string userId, bool includeStalePremiumClaim)
{
var claims = new List<Claim> { new(ClaimTypes.NameIdentifier, userId) };
if (includeStalePremiumClaim) claims.Add(new(ClaimTypes.Role, "Premium"));
return new AuthorizationHandlerContext(
new[] { new ProEntitlementRequirement() },
new ClaimsPrincipal(new ClaimsIdentity(claims, "local")),
null);
}
}