using JobTrackerApi.Models; using JobTrackerApi.Services; using JobTrackerApi.Tests.TestSupport; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Moq; using Xunit; namespace JobTrackerApi.Tests; public sealed class AvatarRetentionTests { [Fact] public async Task Hourly_retention_removes_old_orphans_but_preserves_current_recent_and_unrelated_files() { var root = Path.Combine(Path.GetTempPath(), "avatar-retention-" + Guid.NewGuid().ToString("N")); try { var paths = Paths(root); await using var db = TestHostFactory.CreateInMemoryDb(); var current = await AvatarStorage.StageAsync(root, "owner", new byte[] { 1 }, default); var orphan = await AvatarStorage.StageAsync(root, "owner", new byte[] { 2 }, default); var recent = await AvatarStorage.StageAsync(root, "owner", new byte[] { 3 }, default); var missingOwner = await AvatarStorage.StageAsync(root, "deleted-owner", new byte[] { 4 }, default); var legacyOrphan = await AvatarStorage.StoreAsync(root, "owner", new byte[] { 5 }, "image/webp", default); var legacyCurrent = await AvatarStorage.StoreAsync(root, "legacy-owner", new byte[] { 6 }, "image/jpeg", default); var unrelated = Path.Combine(Path.GetDirectoryName(orphan[5..])!, "unrelated.png"); await File.WriteAllTextAsync(unrelated, "keep"); var now = DateTimeOffset.UtcNow; foreach (var file in new[] { current, orphan, missingOwner, legacyOrphan, legacyCurrent }) File.SetLastWriteTimeUtc(file[5..], now.AddDays(-2).UtcDateTime); db.Users.Add(new ApplicationUser { Id = "owner", UserName = "owner", AvatarImageDataUrl = current }); db.Users.Add(new ApplicationUser { Id = "legacy-owner", UserName = "legacy-owner", AvatarImageDataUrl = legacyCurrent }); await db.SaveChangesAsync(); await CvImportRetentionHostedService.SweepAsync(db, paths, now, default); Assert.True(File.Exists(current[5..])); Assert.True(File.Exists(recent[5..])); Assert.True(File.Exists(unrelated)); Assert.False(File.Exists(orphan[5..])); Assert.False(File.Exists(missingOwner[5..])); Assert.False(File.Exists(legacyOrphan[5..])); Assert.True(File.Exists(legacyCurrent[5..])); } finally { if (Directory.Exists(root)) Directory.Delete(root, true); } } [Fact] public async Task Account_inventory_includes_only_its_own_staged_portraits_even_without_a_current_avatar() { var root = Path.Combine(Path.GetTempPath(), "avatar-inventory-" + Guid.NewGuid().ToString("N")); try { var paths = Paths(root); await using var db = TestHostFactory.CreateInMemoryDb(); var own = await AvatarStorage.StageAsync(root, "owner", new byte[] { 1 }, default); var other = await AvatarStorage.StageAsync(root, "other", new byte[] { 2 }, default); db.Users.Add(new ApplicationUser { Id = "owner", UserName = "owner" }); await db.SaveChangesAsync(); var inventory = await new AccountOwnedFileInventory(db, paths, Mock.Of()).BuildAsync("owner", default); var avatar = Assert.Single(inventory.Files, file => file.Category == "avatar"); Assert.Equal(own[5..], avatar.SourcePath); Assert.DoesNotContain(inventory.Files, file => file.SourcePath == other[5..]); } finally { if (Directory.Exists(root)) Directory.Delete(root, true); } } private static AppPaths Paths(string root) => new( new ConfigurationBuilder().AddInMemoryCollection(new Dictionary { ["Data:Root"] = root }).Build(), Mock.Of(environment => environment.ContentRootPath == root)); [Theory] [InlineData("image/png")] [InlineData("image/jpeg")] [InlineData("image/webp")] public async Task Account_inventory_covers_exact_legacy_leftovers_after_portrait_replacement(string contentType) { var root = Path.Combine(Path.GetTempPath(), "avatar-legacy-inventory-" + Guid.NewGuid().ToString("N")); try { var paths = Paths(root); await using var db = TestHostFactory.CreateInMemoryDb(); var legacy = await AvatarStorage.StoreAsync(root, "owner", new byte[] { 1 }, contentType, default); var current = await AvatarStorage.StageAsync(root, "owner", new byte[] { 2 }, default); db.Users.Add(new ApplicationUser { Id = "owner", UserName = "owner", AvatarImageDataUrl = current }); await db.SaveChangesAsync(); var inventory = await new AccountOwnedFileInventory(db, paths, Mock.Of()).BuildAsync("owner", default); Assert.Contains(inventory.Files, file => file.SourcePath == legacy[5..]); Assert.Single(inventory.Files, file => file.SourcePath == current[5..]); } finally { if (Directory.Exists(root)) Directory.Delete(root, true); } } }