121 lines
5.4 KiB
C#
121 lines
5.4 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(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<AuthorizeAttribute>(),
|
|
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);
|
|
}
|
|
}
|