157 lines
6.6 KiB
C#
157 lines
6.6 KiB
C#
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;
|
|
}
|
|
}
|