fix(categories): correct all seven smart-folder bugs at the source (PHASE 2)
CI / backend (pull_request) Successful in 56s
CI / frontend (pull_request) Successful in 12s
CI / format (pull_request) Successful in 52s
CI / db-tests (pull_request) Successful in 57s
Security / secrets (pull_request) Successful in 3s
Security / dependencies (pull_request) Successful in 57s
Security / sast (pull_request) Successful in 37s
CI / backend (pull_request) Successful in 56s
CI / frontend (pull_request) Successful in 12s
CI / format (pull_request) Successful in 52s
CI / db-tests (pull_request) Successful in 57s
Security / secrets (pull_request) Successful in 3s
Security / dependencies (pull_request) Successful in 57s
Security / sast (pull_request) Successful in 37s
Root causes + fixes: - Archive leaked Sent mail: filter was just 'not in inbox'. Added ExcludeGmailLabels; Archive now excludes SENT/DRAFT/SPAM/TRASH/CHAT by label. - Read Later / Pinned / Unlabelled returned EVERYTHING (no folder case -> default all). Pinned = IsImportant; Read Later = new local IsReadLater marker (+toggle endpoints); Unlabelled = HasUserLabels=false. - Trash & Spam always empty: IncludeSpamTrash=false + sync never set IsTrashed nor created EmailLabel rows. Now fetches spam/trash, sets IsTrashed from the TRASH label, and links every message to its Gmail labels (also fixes Sent/Spam/category-by-label/Unlabelled). - Old Mail: now strictly older than 6 months (was 1 year). Guardrail: sync upsert is now idempotent (drops prior copy before reinsert) so re-sync can't duplicate a message or leave stale labels. 4 new filter tests; 65 backend + frontend green; migration is a single non-destructive column add. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
using FluentAssertions;
|
||||
using InboxIntel.Application.Abstractions;
|
||||
using InboxIntel.Application.DTOs;
|
||||
using InboxIntel.Domain.Entities;
|
||||
using InboxIntel.Infrastructure.Persistence;
|
||||
using InboxIntel.Infrastructure.Search;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Xunit;
|
||||
|
||||
namespace InboxIntel.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// PHASE 2 (smart-category correctness): each reported folder bug — Archive leaking Sent,
|
||||
/// Read Later / Pinned / Unlabelled returning everything, Old Mail duration — is fixed at the
|
||||
/// filter source. These prove the SearchService filters that back the corrected folder mappings.
|
||||
/// </summary>
|
||||
public class CategoryFilterTests
|
||||
{
|
||||
private sealed class FakeCurrentUser : ICurrentUser
|
||||
{
|
||||
public Guid UserId { get; set; }
|
||||
public bool IsAuthenticated => UserId != Guid.Empty;
|
||||
}
|
||||
|
||||
private static AppDbContext Db(string name, Guid uid) =>
|
||||
new(new DbContextOptionsBuilder<AppDbContext>().UseInMemoryDatabase(name).Options,
|
||||
new FakeCurrentUser { UserId = uid });
|
||||
|
||||
// A small fixture: an inbox email, a sent email (SENT label), one important, one read-later,
|
||||
// and one filed under a user label.
|
||||
private static async Task<Guid> SeedAsync(string name)
|
||||
{
|
||||
var uid = Guid.NewGuid();
|
||||
using var db = Db(name, Guid.Empty);
|
||||
var sender = new Sender { UserId = uid, Address = "s@x.x" };
|
||||
db.Senders.Add(sender);
|
||||
var sentLabel = new Label { UserId = uid, GmailLabelId = "SENT", Name = "Sent", Type = "system" };
|
||||
var userLabel = new Label { UserId = uid, GmailLabelId = "Label_1", Name = "Projects", Type = "user" };
|
||||
db.Labels.AddRange(sentLabel, userLabel);
|
||||
|
||||
Email E(string id, Action<Email> cfg)
|
||||
{
|
||||
var e = new Email { UserId = uid, GmailMessageId = id, Subject = id, Sender = sender, SentAtUtc = DateTimeOffset.UtcNow };
|
||||
cfg(e);
|
||||
db.Emails.Add(e);
|
||||
return e;
|
||||
}
|
||||
var inbox = E("inbox", e => e.IsInInbox = true);
|
||||
var sent = E("sent", e => e.IsInInbox = false); // archived-looking, but it's Sent
|
||||
var important = E("important", e => e.IsImportant = true);
|
||||
var later = E("later", e => e.IsReadLater = true);
|
||||
var filed = E("filed", e => e.IsInInbox = false);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
db.Set<EmailLabel>().AddRange(
|
||||
new EmailLabel { EmailId = sent.Id, LabelId = sentLabel.Id },
|
||||
new EmailLabel { EmailId = filed.Id, LabelId = userLabel.Id });
|
||||
await db.SaveChangesAsync();
|
||||
return uid;
|
||||
}
|
||||
|
||||
private static async Task<List<string>> RunAsync(string name, Guid uid, SearchRequestDto req)
|
||||
{
|
||||
using var db = Db(name, uid);
|
||||
var res = await new SearchService(db).SearchAsync(uid, req);
|
||||
return res.Items.Select(i => i.GmailMessageId).OrderBy(x => x).ToList();
|
||||
}
|
||||
|
||||
private static SearchRequestDto Base() => new(null, null, null, null, null, null, null, false, 1, 50);
|
||||
|
||||
[Fact]
|
||||
public async Task Archive_excludes_sent_mail()
|
||||
{
|
||||
var name = nameof(Archive_excludes_sent_mail);
|
||||
var uid = await SeedAsync(name);
|
||||
// Archive: not-in-inbox, not-trashed, excluding SENT — must NOT contain "sent".
|
||||
var items = await RunAsync(name, uid, Base() with
|
||||
{
|
||||
IsInInbox = false,
|
||||
IsTrashed = false,
|
||||
ExcludeGmailLabels = new[] { "SENT", "SPAM", "DRAFT" }
|
||||
});
|
||||
items.Should().NotContain("sent");
|
||||
items.Should().Contain("filed"); // a genuinely archived, user-filed mail stays
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Pinned_returns_only_important_not_everything()
|
||||
{
|
||||
var name = nameof(Pinned_returns_only_important_not_everything);
|
||||
var uid = await SeedAsync(name);
|
||||
var items = await RunAsync(name, uid, Base() with { IsImportant = true });
|
||||
items.Should().Equal("important");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadLater_returns_only_flagged_not_everything()
|
||||
{
|
||||
var name = nameof(ReadLater_returns_only_flagged_not_everything);
|
||||
var uid = await SeedAsync(name);
|
||||
var items = await RunAsync(name, uid, Base() with { IsReadLater = true });
|
||||
items.Should().Equal("later");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Unlabelled_excludes_user_labelled_mail()
|
||||
{
|
||||
var name = nameof(Unlabelled_excludes_user_labelled_mail);
|
||||
var uid = await SeedAsync(name);
|
||||
// "filed" has a user label → must be absent; everything else (no user label) present.
|
||||
var items = await RunAsync(name, uid, Base() with { HasUserLabels = false });
|
||||
items.Should().NotContain("filed");
|
||||
items.Should().Contain("inbox");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user