- Request entity (GmailMessageId unique, ThreadId, threading headers, From*, Subject, BodyText,
ReceivedAt, Status enum-as-string New|InProgress|Assigned|Answered|Abandoned, AssignedContactId? FK,
DraftReply?, AnsweredAt?). MailSettings singleton (GmailAddress, AppPassword ENCRYPTED via Data
Protection, PollInterval, Folder, LastUid). Migration AddRequestsAndMailSettings.
- IGmailMailReader (mockable) + MailKit GmailMailReader (imap.gmail.com:993 SSL, envelope+X-GM-THRID
+body). RequestSyncService: dedup on Message-Id, track LastUid. MailKit 4.17.0.
- Endpoints: POST /api/requests/sync, GET /api/requests (Gridify, filter status), GET /api/requests/{id},
PUT triage (status+assignedContactId), GET/PUT /api/mail-settings (App Password never returned).
- Tests (in-memory DB + canned reader): sync dedup/idempotency/config-guard, HTTP sync->triage->assign,
mail-settings secrecy. + gitignore the runtime photo-storage/ dir (god housekeeping).
87 lines
3.3 KiB
C#
87 lines
3.3 KiB
C#
using GerbilManagerWebAPI.Inbox;
|
|
using GerbilManagerWebAPI.Models;
|
|
using Microsoft.AspNetCore.DataProtection;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace GerbilManager.Tests;
|
|
|
|
/// <summary>INBOX-0: sync dedup + idempotency + config-guard, against an in-memory DB and a
|
|
/// fake IGmailMailReader (the live Gmail/MailKit path is gated on Julian's App Password).</summary>
|
|
public class RequestSyncServiceTests
|
|
{
|
|
private static ApplicationContext NewDb()
|
|
{
|
|
var opts = new DbContextOptionsBuilder<ApplicationContext>()
|
|
.UseInMemoryDatabase("inbox-" + Guid.NewGuid().ToString("N")).Options;
|
|
var db = new ApplicationContext(opts);
|
|
db.Database.EnsureCreated(); // seeds the MailSettings singleton
|
|
return db;
|
|
}
|
|
|
|
private sealed class FakeReader : IGmailMailReader
|
|
{
|
|
public List<MailSummary> Canned { get; set; } = new();
|
|
public Task<IReadOnlyList<MailSummary>> FetchAsync(MailConnection c, uint sinceUid, CancellationToken ct = default)
|
|
=> Task.FromResult((IReadOnlyList<MailSummary>)Canned.Where(m => m.Uid > sinceUid).ToList());
|
|
}
|
|
|
|
private static MailSummary M(uint uid, string msgId, string from) =>
|
|
new(msgId, ThreadId: null, InReplyTo: null, References: null,
|
|
FromAddress: from, FromName: null, Subject: "Anfrage", BodyText: "Hallo",
|
|
ReceivedAt: DateTimeOffset.UtcNow, Uid: uid);
|
|
|
|
private static async Task<(ApplicationContext db, RequestSyncService sync, FakeReader reader)> SetupAsync(bool configured = true)
|
|
{
|
|
var db = NewDb();
|
|
var settings = new MailSettingsService(db, new EphemeralDataProtectionProvider());
|
|
var s = await settings.GetAsync();
|
|
if (configured)
|
|
{
|
|
s.GmailAddress = "zucht@gmail.com";
|
|
settings.SetPassword(s, "app-pw-1234");
|
|
await db.SaveChangesAsync();
|
|
}
|
|
var reader = new FakeReader();
|
|
return (db, new RequestSyncService(db, reader, settings), reader);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Sync_imports_new_and_dedups_within_batch()
|
|
{
|
|
var (db, sync, reader) = await SetupAsync();
|
|
reader.Canned = new() { M(1, "<a@x>", "a@x.de"), M(2, "<b@y>", "b@y.de"), M(3, "<b@y>", "b@y.de") };
|
|
|
|
var res = await sync.SyncAsync();
|
|
|
|
Assert.Null(res.Error);
|
|
Assert.Equal(2, res.Imported); // <b@y> deduped
|
|
Assert.Equal(2, await db.Requests.CountAsync());
|
|
Assert.All(await db.Requests.ToListAsync(), r => Assert.Equal(RequestStatus.New, r.Status));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Resync_imports_nothing_new()
|
|
{
|
|
var (db, sync, reader) = await SetupAsync();
|
|
reader.Canned = new() { M(1, "<a@x>", "a@x.de"), M(2, "<b@y>", "b@y.de") };
|
|
await sync.SyncAsync();
|
|
|
|
var second = await sync.SyncAsync(); // LastUid advanced -> reader returns nothing newer
|
|
|
|
Assert.Equal(0, second.Imported);
|
|
Assert.Equal(2, await db.Requests.CountAsync());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Sync_without_config_reports_not_configured()
|
|
{
|
|
var (_, sync, reader) = await SetupAsync(configured: false);
|
|
reader.Canned = new() { M(1, "<a@x>", "a@x.de") };
|
|
|
|
var res = await sync.SyncAsync();
|
|
|
|
Assert.Equal("MailNotConfigured", res.Error);
|
|
Assert.Equal(0, res.Imported);
|
|
}
|
|
}
|