feat(applications): preserve submitted packages

This commit is contained in:
cesnimda
2026-08-31 16:54:41 +02:00
parent 47e8e3e50e
commit 5337ba3a5e
22 changed files with 903 additions and 11 deletions
@@ -0,0 +1,156 @@
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<string, string?> { ["Data:Root"] = root }).Build();
var environment = new Mock<IHostEnvironment>();
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<string>(), It.IsAny<CancellationToken>()), 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<string>())).Returns(true);
await Assert.ThrowsAsync<IOException>(() => service.CaptureAsync("owner-1", job.Id, new CvRenderPerson("Ada", null), default));
Assert.Empty(db.SubmittedApplicationPackages);
}
private static (JobTrackerContext Db, SubmittedApplicationPackageService Service, Mock<IAttachmentStorage> Storage) New(string userId)
{
var currentUser = new Mock<ICurrentUserService>();
currentUser.SetupGet(x => x.UserId).Returns(userId);
var options = new DbContextOptionsBuilder<JobTrackerContext>().UseInMemoryDatabase(Guid.NewGuid().ToString()).Options;
var db = new JobTrackerContext(options, currentUser.Object);
var variants = new Mock<ICvVariantService>();
variants.Setup(x => x.RenderAsync(userId, It.IsAny<int>(), It.IsAny<CvRenderPerson>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new ThemedCvRenderResult("nordic", "cv.pdf", "<html>Frozen CV</html>"));
var storage = new Mock<IAttachmentStorage>();
storage.Setup(x => x.IsManagedPath(It.IsAny<string>())).Returns(true);
storage.Setup(x => x.CreatePackagePath(It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string>())).Returns("snapshot.pdf");
storage.Setup(x => x.SnapshotAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new StoredSnapshotFile("snapshot.pdf", 21, new string('a', 64)));
return (db, new SubmittedApplicationPackageService(db, variants.Object, storage.Object), storage);
}
private static async Task<JobApplication> 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;
}
}