using FluentAssertions; using InboxIntel.Application.Abstractions; using InboxIntel.Domain.Entities; using InboxIntel.Infrastructure.Configuration; using InboxIntel.Infrastructure.Persistence; using InboxIntel.Infrastructure.Retention; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Xunit; namespace InboxIntel.IntegrationTests; /// /// AUDIT H-3 (retention): the purge removes only what the configuration targets and is a /// no-op while disabled — locking the "no behaviour change until opted in" guarantee. /// public class RetentionServiceTests { private sealed class FakeCurrentUser : ICurrentUser { public Guid UserId => Guid.Empty; // worker scope public bool IsAuthenticated => false; } private static AppDbContext NewDb(string name) => new(new DbContextOptionsBuilder().UseInMemoryDatabase(name).Options, new FakeCurrentUser()); private static async Task SeedAsync(AppDbContext db) { var user = Guid.NewGuid(); db.Emails.AddRange( new Email { UserId = user, GmailMessageId = "old-trashed", IsTrashed = true, SentAtUtc = DateTimeOffset.UtcNow.AddDays(-100) }, new Email { UserId = user, GmailMessageId = "new-trashed", IsTrashed = true, SentAtUtc = DateTimeOffset.UtcNow.AddDays(-5) }, new Email { UserId = user, GmailMessageId = "old-kept", IsTrashed = false, SentAtUtc = DateTimeOffset.UtcNow.AddDays(-100) }); await db.SaveChangesAsync(); } private static RetentionService Sut(AppDbContext db, int trashedDays = 0, int allDays = 0) => new(db, Options.Create(new DataRetentionOptions { PurgeTrashedAfterDays = trashedDays, PurgeAllAfterDays = allDays }), NullLogger.Instance); [Fact] public async Task Disabled_retention_purges_nothing() { using var db = NewDb(nameof(Disabled_retention_purges_nothing)); await SeedAsync(db); (await Sut(db).PurgeAsync()).Should().Be(0); (await db.Emails.CountAsync()).Should().Be(3); } [Fact] public async Task Trashed_purge_removes_only_old_trashed_emails() { using var db = NewDb(nameof(Trashed_purge_removes_only_old_trashed_emails)); await SeedAsync(db); (await Sut(db, trashedDays: 30).PurgeAsync()).Should().Be(1); var remaining = await db.Emails.Select(e => e.GmailMessageId).ToListAsync(); remaining.Should().BeEquivalentTo(new[] { "new-trashed", "old-kept" }); } [Fact] public async Task Age_purge_removes_everything_past_the_cutoff() { using var db = NewDb(nameof(Age_purge_removes_everything_past_the_cutoff)); await SeedAsync(db); (await Sut(db, allDays: 30).PurgeAsync()).Should().Be(2); // both 100-day-old emails (await db.Emails.SingleAsync()).GmailMessageId.Should().Be("new-trashed"); } }