fix(workspace): clear native package and navigation readiness gates

This commit is contained in:
cesnimda
2026-10-07 00:30:10 +02:00
parent fadf99a3f5
commit acd1d94684
36 changed files with 1551 additions and 40 deletions
@@ -1,4 +1,8 @@
using System.Security.Claims;
using System.IO.Compression;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using JobTrackerApi.Controllers;
using JobTrackerApi.Data;
using JobTrackerApi.Models;
@@ -17,6 +21,79 @@ namespace JobTrackerApi.Tests;
public sealed class SqliteDateTimeOffsetCompatibilityTests
{
[Fact]
public async Task Tailoring_and_submission_capture_work_on_native_sqlite()
{
await using var fixture = await SqliteFixture.CreateAsync("user-1");
var db = fixture.Db;
var company = new Company { OwnerUserId = "user-1", Name = "Nordlys AS" };
db.Companies.Add(company);
await db.SaveChangesAsync();
var job = new JobApplication { OwnerUserId = "user-1", CompanyId = company.Id, JobTitle = "Engineer", Status = "Saved", Description = "C# engineer", CoverLetterText = "Søknad — æøå" };
db.JobApplications.Add(job);
await db.SaveChangesAsync();
db.CvVariants.Add(Variant("user-1", job.Id, "Submitted CV", DateTimeOffset.UtcNow));
db.Users.Add(new ApplicationUser { Id = "user-1", UserName = "native@example.test" });
var storage = new AttachmentStorage(fixture.Paths);
var source = storage.CreateFinalPath(job.Id, "evidence.txt");
var bytes = Encoding.UTF8.GetBytes("Preserved evidence — æøå\n");
await File.WriteAllBytesAsync(source, bytes);
var attachment = new Attachment { JobApplicationId = job.Id, FileName = "evidence.txt", FilePath = source, FileType = "text/plain", FileSize = bytes.Length, Purpose = "portfolio" };
db.Attachments.Add(attachment);
await db.SaveChangesAsync();
var career = new CareerProfileService(db);
await career.SaveVersionAsync("user-1", JsonSerializer.Deserialize<StructuredCvProfile>("{\"version\":\"1\",\"contact\":{\"fullName\":\"Åse\",\"headline\":\"Original profile\"}}", new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!, "manual", default);
var variants = new CvVariantService(db, career, new ThemedCvRenderer());
var assets = new ApplicationAssetsService(db, variants, new ApplicationIntelligenceService(db, new JobCvMatchService()));
Assert.NotNull(await assets.GetTailoringPlanAsync("user-1", job.Id, default));
var packages = new SubmittedApplicationPackageService(db, variants, storage);
var captured = await packages.CaptureAsync("user-1", job.Id, new CvRenderPerson("Åse", null), default);
Assert.NotNull(captured);
Assert.Equal("Søknad — æøå", (await packages.GetAsync("user-1", job.Id, captured.Id, default))!.CoverLetterText);
Assert.Null(await packages.GetAsync("user-2", job.Id, captured.Id, default));
var before = await packages.GetAsync("user-1", job.Id, captured.Id, default);
Assert.Contains("Original profile", before!.RenderedCvHtml);
var frozenFile = Assert.Single(captured.Attachments);
Assert.Equal(Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant(), frozenFile.Sha256);
Assert.Null(await packages.GetAttachmentAsync("user-2", job.Id, captured.Id, frozenFile.Id, default));
job.CoverLetterText = "Later letter";
(await db.CareerProfiles.SingleAsync()).ProfileJson = "{}";
(await db.CvVariants.SingleAsync()).Name = "Later CV";
db.Attachments.Remove(attachment);
await db.SaveChangesAsync();
File.Delete(source);
var after = await packages.GetAsync("user-1", job.Id, captured.Id, default);
Assert.Equal(before.RenderedCvHtml, after!.RenderedCvHtml);
Assert.Equal(before.CoverLetterText, after.CoverLetterText);
Assert.Equal(before.ApplicationMaterialJson, after.ApplicationMaterialJson);
Assert.Equal(before.Summary.CvVariantVersion, after.Summary.CvVariantVersion);
var downloaded = await packages.GetAttachmentAsync("user-1", job.Id, captured.Id, frozenFile.Id, default);
Assert.Equal(bytes, await File.ReadAllBytesAsync(downloaded!.Path));
var exporter = new AccountDataExportService(db, fixture.Paths, new AccountOwnedFileInventory(db, fixture.Paths, storage), TimeProvider.System);
var exported = await exporter.CreateAsync("user-1", default);
using var zip = ZipFile.OpenRead(exported.StoragePath);
using var workspaceStream = zip.GetEntry("data/application-workspace.json")!.Open();
using var workspace = await JsonDocument.ParseAsync(workspaceStream);
var frozen = Assert.Single(workspace.RootElement.GetProperty("submittedApplicationPackages").EnumerateArray());
Assert.Equal(captured.Id, frozen.GetProperty("id").GetInt32());
Assert.Equal(job.Id, frozen.GetProperty("jobApplicationId").GetInt32());
Assert.Equal(before.RenderedCvHtml, frozen.GetProperty("renderedCvHtml").GetString());
Assert.Equal(before.CoverLetterText, frozen.GetProperty("coverLetterText").GetString());
Assert.Equal(before.ApplicationMaterialJson, frozen.GetProperty("applicationMaterialJson").GetString());
Assert.Equal("Submitted CV", frozen.GetProperty("cvVariantName").GetString());
Assert.Contains("Original profile", frozen.GetProperty("careerProfileJson").GetString());
Assert.Equal("portfolio", Assert.Single(frozen.GetProperty("attachments").EnumerateArray()).GetProperty("purpose").GetString());
var path = $"files/submitted-packages/{captured.Id}/{frozenFile.Id}/evidence.txt";
using var entry = zip.GetEntry(path)!.Open();
using var readback = new MemoryStream();
await entry.CopyToAsync(readback);
Assert.Equal(bytes, readback.ToArray());
using var manifestStream = zip.GetEntry("manifest.json")!.Open();
using var manifest = await JsonDocument.ParseAsync(manifestStream);
var metadata = manifest.RootElement.GetProperty("entries").EnumerateArray().Single(x => x.GetProperty("path").GetString() == path);
Assert.Equal(frozenFile.Sha256, metadata.GetProperty("sha256").GetString());
}
[Fact]
public void MariaDb_branch_translates_the_same_date_ordering_and_range_queries()
{
@@ -37,6 +114,40 @@ public sealed class SqliteDateTimeOffsetCompatibilityTests
Assert.Contains("CreatedAtUtc", rangeSql, StringComparison.Ordinal);
}
[Fact]
public async Task Native_sparse_capture_and_missing_attachment_retry_never_publish_partial_packages()
{
await using var fixture = await SqliteFixture.CreateAsync("user-1");
var db = fixture.Db;
var company = new Company { OwnerUserId = "user-1", Name = "Sparse AS" };
db.Companies.Add(company);
await db.SaveChangesAsync();
var job = new JobApplication { OwnerUserId = "user-1", CompanyId = company.Id, JobTitle = "Sparse role", Status = "Saved" };
db.JobApplications.Add(job);
await db.SaveChangesAsync();
var storage = new AttachmentStorage(fixture.Paths);
var packages = new SubmittedApplicationPackageService(db, new CvVariantService(db, new CareerProfileService(db), new ThemedCvRenderer()), storage);
var sparse = await packages.CaptureAsync("user-1", job.Id, new CvRenderPerson("Åse", null), default);
Assert.False(sparse!.HasCv);
Assert.False(sparse.HasCoverLetter);
Assert.Empty(sparse.Attachments);
var availableSource = storage.CreateFinalPath(job.Id, "available.txt");
await File.WriteAllTextAsync(availableSource, "Already available native bytes");
db.Attachments.Add(new Attachment { JobApplicationId = job.Id, FileName = "available.txt", FilePath = availableSource, FileType = "text/plain" });
await db.SaveChangesAsync();
var source = storage.CreateFinalPath(job.Id, "retry.txt");
db.Attachments.Add(new Attachment { JobApplicationId = job.Id, FileName = "retry.txt", FilePath = source, FileType = "text/plain" });
await db.SaveChangesAsync();
await Assert.ThrowsAsync<IOException>(() => packages.CaptureAsync("user-1", job.Id, new CvRenderPerson("Åse", null), default));
Assert.Single((await packages.ListAsync("user-1", job.Id, default))!);
Assert.Empty(Directory.GetFiles(Path.Combine(fixture.Paths.AttachmentsRoot, job.Id.ToString(), "submitted", "2")));
await File.WriteAllTextAsync(source, "Retried native bytes");
var retried = await packages.CaptureAsync("user-1", job.Id, new CvRenderPerson("Åse", null), default);
Assert.Equal(2, retried!.Version);
Assert.Equal(2, retried.Attachments.Count);
Assert.Equal(2, (await packages.ListAsync("user-1", job.Id, default))!.Count);
}
[Fact]
public async Task Affected_workspace_queries_order_dates_and_preserve_owner_scope()
{