using System.Security.Claims; using JobTrackerApi.Controllers; using JobTrackerApi.Data; using JobTrackerApi.Models; using JobTrackerApi.Services; using JobTrackerApi.Tests.TestSupport; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Moq; using Xunit; namespace JobTrackerApi.Tests; public sealed class CoverLetterThemeEntitlementTests { [Theory] [InlineData("save")] [InlineData("preview")] [InlineData("restore")] public async Task Free_user_cannot_activate_a_premium_version_two_letter_through_any_design_writer(string action) { var (db, job, controller, _) = await Fixture(); await using var dispose = db; var premium = new CoverLetterDocumentSettings { ThemeId = "executive", DesignVersion = 2 }; db.CoverLetterVersions.Add(new CoverLetterVersion { OwnerUserId = "owner", JobApplicationId = job.Id, Version = 1, Text = "Historical premium", Source = CoverLetterSources.Manual, DocumentSettingsJson = CoverLetterDocumentSettingsJson.Serialize(premium) }); await db.SaveChangesAsync(); var response = action switch { "preview" => (await controller.PreviewCoverLetter(job.Id, new("Reviewed text", premium), default)).Result, "restore" => (await controller.RestoreCoverLetter(job.Id, 1, new(job.Version), default)).Result, _ => (await controller.SaveCoverLetter(job.Id, new("Reviewed text", "manual", null, job.Version, premium), default)).Result, }; Assert.Equal(403, Assert.IsAssignableFrom(response).StatusCode); Assert.Null(job.CoverLetterDocumentSettingsJson); Assert.Null(job.CoverLetterText); } [Fact] public async Task Shared_service_rejects_premium_design_before_changing_letter_or_history() { var (db, job, _, service) = await Fixture(); await using var dispose = db; await Assert.ThrowsAsync(() => service.SaveCoverLetterAsync("owner", job.Id, "Unsaved", "manual", null, job.Version, default, new() { DesignVersion = 2, ThemeId = "technical" })); Assert.Null(job.CoverLetterText); Assert.Null(job.CoverLetterDocumentSettingsJson); Assert.Empty(db.CoverLetterVersions); } [Theory] [InlineData(null)] [InlineData(1)] public async Task Legacy_letter_metadata_remains_editable_and_previewable_for_free_users(int? designVersion) { var (db, job, controller, _) = await Fixture(); await using var dispose = db; var legacy = new CoverLetterDocumentSettings { ThemeId = "executive", DesignVersion = designVersion }; Assert.IsType((await controller.PreviewCoverLetter(job.Id, new("Legacy text", legacy), default)).Result); Assert.IsType((await controller.SaveCoverLetter(job.Id, new("Legacy text", "manual", null, job.Version, legacy), default)).Result); } [Fact] public async Task Premium_account_can_save_and_preview_a_new_premium_letter_design() { var (db, job, controller, service) = await Fixture(premium: true); await using var dispose = db; var design = new CoverLetterDocumentSettings { ThemeId = "technical", DesignVersion = 2 }; Assert.IsType((await controller.PreviewCoverLetter(job.Id, new("Reviewed text", design), default)).Result); Assert.IsType((await controller.SaveCoverLetter(job.Id, new("Reviewed text", "manual", null, job.Version, design), default)).Result); Assert.NotNull(await service.RenderSavedCoverLetterAsync("owner", job.Id, job.Version, default)); } [Fact] public async Task Downgraded_account_keeps_current_premium_design_but_cannot_switch_to_another_premium_design() { var (db, job, controller, service) = await Fixture(); await using var dispose = db; var stored = new CoverLetterDocumentSettings { ThemeId = "executive", DesignVersion = 2 }; job.CoverLetterDocumentSettingsJson = CoverLetterDocumentSettingsJson.Serialize(stored); job.CoverLetterText = "Stored premium text"; await db.SaveChangesAsync(); Assert.NotNull(await service.GetCoverLetterAsync("owner", job.Id, default)); Assert.NotNull(await service.RenderSavedCoverLetterAsync("owner", job.Id, job.Version, default)); Assert.IsType((await controller.PreviewCoverLetter(job.Id, new("Text edit", stored), default)).Result); Assert.IsType((await controller.SaveCoverLetter(job.Id, new("Text edit", "manual", null, job.Version, stored), default)).Result); var different = new CoverLetterDocumentSettings { ThemeId = "technical", DesignVersion = 2 }; Assert.Equal(403, Assert.IsAssignableFrom((await controller.PreviewCoverLetter(job.Id, new("Text edit", different), default)).Result).StatusCode); Assert.Equal(403, Assert.IsAssignableFrom((await controller.SaveCoverLetter(job.Id, new("Text edit", "manual", null, job.Version, different), default)).Result).StatusCode); Assert.Equal("executive", CoverLetterDocumentSettingsJson.Deserialize(job.CoverLetterDocumentSettingsJson).ThemeId); } [Fact] public async Task Foreign_application_remains_not_found_before_entitlement_checks() { var (db, job, controller, _) = await Fixture(); await using var dispose = db; job.OwnerUserId = "other"; await db.SaveChangesAsync(); var design = new CoverLetterDocumentSettings { ThemeId = "executive", DesignVersion = 2 }; Assert.IsType((await controller.PreviewCoverLetter(job.Id, new("Foreign", design), default)).Result); Assert.IsType((await controller.SaveCoverLetter(job.Id, new("Foreign", "manual", null, job.Version, design), default)).Result); } private static async Task<(JobTrackerContext db, JobApplication job, ApplicationAssetsController controller, ApplicationAssetsService service)> Fixture(bool premium = false) { var db = TestHostFactory.CreateInMemoryDb("owner"); var user = new ApplicationUser { Id = "owner", UserName = "owner" }; db.Users.Add(user); if (premium) { db.Roles.Add(new IdentityRole("Premium") { Id = "premium-role" }); db.UserRoles.Add(new IdentityUserRole { UserId = user.Id, RoleId = "premium-role" }); } var company = new Company { OwnerUserId = user.Id, Name = "Synthetic" }; var job = new JobApplication { OwnerUserId = user.Id, Company = company, JobTitle = "Synthetic role" }; db.JobApplications.Add(job); await db.SaveChangesAsync(); var service = new ApplicationAssetsService(db, Mock.Of(), Mock.Of()); var users = TestHostFactory.CreateUserManager(); users.Setup(manager => manager.GetUserAsync(It.IsAny())).ReturnsAsync(user); var controller = new ApplicationAssetsController(users.Object, service, Mock.Of()) { ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() } }; return (db, job, controller, service); } }