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()), 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(JobApplicationsController), nameof(JobApplicationsController.GetFocusPlan))] [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); Assert.Contains(method!.GetCustomAttributes(typeof(AuthorizeAttribute), true).Cast(), 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>( Mock.Of>(), 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 { 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); } }