using JobTrackerApi.Data; using JobTrackerApi.Models; using JobTrackerApi.Services; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Moq; using Xunit; namespace JobTrackerApi.Tests; public sealed class SubmittedApplicationPackageTests { [Fact] public async Task Storage_snapshot_copies_exact_bytes_and_calculates_sha256() { var root = Path.Combine(Path.GetTempPath(), $"jobtracker-package-{Guid.NewGuid():N}"); try { var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary { ["Data:Root"] = root }).Build(); var environment = new Mock(); environment.SetupGet(x => x.ContentRootPath).Returns(root); var storage = new AttachmentStorage(new AppPaths(config, environment.Object)); var source = storage.CreateFinalPath(7, "source.txt"); await File.WriteAllTextAsync(source, "exact submitted bytes"); var destination = storage.CreatePackagePath(7, 1, "source.txt"); var result = await storage.SnapshotAsync(source, destination, default); Assert.Equal("exact submitted bytes", await File.ReadAllTextAsync(destination)); Assert.Equal("32283fd0838392ef9d8c707f521096966432939d5d83624bbfce150cde3ce26c", result.Sha256); } finally { if (Directory.Exists(root)) Directory.Delete(root, recursive: true); } } [Fact] public async Task Capture_freezes_documents_and_attachment_hashes() { var temp = Path.GetTempFileName(); await File.WriteAllTextAsync(temp, "exact submitted bytes"); try { var (db, service, storage) = New("owner-1"); await using var _ = db; var job = await SeedAsync(db, "owner-1", temp); var result = await service.CaptureAsync("owner-1", job.Id, new CvRenderPerson("Ada Example", null), default); Assert.NotNull(result); Assert.Equal(1, result.Version); Assert.True(result.HasCv); Assert.True(result.HasCoverLetter); Assert.Single(result.Attachments); Assert.Equal(new string('a', 64), result.Attachments[0].Sha256); var stored = await db.SubmittedApplicationPackages.Include(x => x.Attachments).SingleAsync(); Assert.Contains("Frozen CV", stored.RenderedCvHtml); Assert.Contains("Original profile", stored.CareerProfileJson); Assert.Contains("Application answer", stored.ApplicationMaterialJson); storage.Verify(x => x.SnapshotAsync(temp, It.IsAny(), It.IsAny()), Times.Once); } finally { File.Delete(temp); } } [Fact] public async Task Capture_is_tenant_scoped() { var (db, service, _) = New("owner-1"); await using var dispose = db; var job = await SeedAsync(db, "owner-1", null); Assert.Null(await service.CaptureAsync("owner-2", job.Id, new CvRenderPerson("Other", null), default)); Assert.Empty(db.SubmittedApplicationPackages); } [Fact] public async Task Missing_attachment_aborts_the_whole_snapshot() { var (db, service, storage) = New("owner-1"); await using var dispose = db; var job = await SeedAsync(db, "owner-1", "missing-file.pdf"); storage.Setup(x => x.IsManagedPath(It.IsAny())).Returns(true); await Assert.ThrowsAsync(() => service.CaptureAsync("owner-1", job.Id, new CvRenderPerson("Ada", null), default)); Assert.Empty(db.SubmittedApplicationPackages); } private static (JobTrackerContext Db, SubmittedApplicationPackageService Service, Mock Storage) New(string userId) { var currentUser = new Mock(); currentUser.SetupGet(x => x.UserId).Returns(userId); var options = new DbContextOptionsBuilder().UseInMemoryDatabase(Guid.NewGuid().ToString()).Options; var db = new JobTrackerContext(options, currentUser.Object); var variants = new Mock(); variants.Setup(x => x.RenderAsync(userId, It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new ThemedCvRenderResult("nordic", "cv.pdf", "Frozen CV")); var storage = new Mock(); storage.Setup(x => x.IsManagedPath(It.IsAny())).Returns(true); storage.Setup(x => x.CreatePackagePath(It.IsAny(), It.IsAny(), It.IsAny())).Returns("snapshot.pdf"); storage.Setup(x => x.SnapshotAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new StoredSnapshotFile("snapshot.pdf", 21, new string('a', 64))); return (db, new SubmittedApplicationPackageService(db, variants.Object, storage.Object), storage); } private static async Task SeedAsync(JobTrackerContext db, string owner, string? attachmentPath) { var company = new Company { OwnerUserId = owner, Name = "Example AS" }; db.Companies.Add(company); await db.SaveChangesAsync(); var job = new JobApplication { OwnerUserId = owner, CompanyId = company.Id, JobTitle = "Engineer", Status = "Applied", CoverLetterText = "Exact cover letter", Notes = "Application answer", }; db.JobApplications.Add(job); await db.SaveChangesAsync(); db.CareerProfiles.Add(new CareerProfile { OwnerUserId = owner, ProfileJson = "{\"summary\":\"Original profile\"}" }); db.CvVariants.Add(new CvVariant { OwnerUserId = owner, JobApplicationId = job.Id, Name = "Backend CV", PublicSlug = Guid.NewGuid().ToString("N"), Version = 4, SettingsJson = CvVariantSettingsJson.Serialize(new CvVariantSettings { ThemeId = "nordic" }), CreatedAtUtc = DateTimeOffset.UtcNow, UpdatedAtUtc = DateTimeOffset.UtcNow, }); if (attachmentPath is not null) { db.Attachments.Add(new Attachment { JobApplicationId = job.Id, FileName = "evidence.pdf", FilePath = attachmentPath, FileType = "application/pdf", FileSize = 21, Purpose = "portfolio", }); } await db.SaveChangesAsync(); return job; } }