cacad5cc94
b2 of the multi-provider email roadmap. Mirrors the Gmail provider's shape end-to-end so the two stay structurally interchangeable: - MicrosoftGraphConnection model + table (reconciler pattern, SQLite+MySQL, same shape as GmailConnection: encrypted refresh/access token, sync state). - MicrosoftGraphOAuthService: auth-code + offline-access flow against login.microsoftonline.com, encrypted token storage via IDataProtector, message search/thread/detail fetch against Microsoft Graph (conversationId stands in for Gmail's threadId), attachment listing. - MicrosoftGraphProvider implements IEmailProvider — no contract changes; the existing seam was already provider-neutral. - MicrosoftGraphController: connect-url/oauth/callback/status/disconnect, mirrors GmailController's OAuth surface exactly (including the popup postMessage handshake). Job-matching/review endpoints stay Gmail-only for now, per the roadmap — generalising those needs the frontend provider picker work, not this slice. - Registered in DI + IEmailProviderRegistry (multi-registration of IEmailProvider, resolved by ProviderKey). - Config: Microsoft:ClientId/ClientSecret/TenantId/RedirectUri, wired through docker-compose.yml + .env.example alongside the existing Google:Gmail* keys. - Tests: MicrosoftGraphControllerTests (OAuth lifecycle) + MicrosoftGraphProviderTests (DTO mapping onto the neutral contract). 147/147 green (135 existing + 12 new). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
83 lines
3.5 KiB
C#
83 lines
3.5 KiB
C#
using JobTrackerApi.Services;
|
|
using JobTrackerApi.Services.EmailProviders;
|
|
using Moq;
|
|
using Xunit;
|
|
|
|
namespace JobTrackerApi.Tests;
|
|
|
|
public sealed class MicrosoftGraphProviderTests
|
|
{
|
|
[Fact]
|
|
public void ProviderKey_is_microsoft()
|
|
{
|
|
var provider = new MicrosoftGraphProvider(Mock.Of<IMicrosoftGraphOAuthService>());
|
|
Assert.Equal("microsoft", provider.ProviderKey);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetConnectionAsync_maps_mail_address_onto_neutral_shape()
|
|
{
|
|
var graph = new Mock<IMicrosoftGraphOAuthService>();
|
|
graph.Setup(service => service.GetConnectionAsync("user-1", It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new JobTrackerApi.Models.MicrosoftGraphConnection { OwnerUserId = "user-1", MailAddress = "user@outlook.test" });
|
|
|
|
var provider = new MicrosoftGraphProvider(graph.Object);
|
|
var connection = await provider.GetConnectionAsync("user-1", CancellationToken.None);
|
|
|
|
Assert.NotNull(connection);
|
|
Assert.Equal("microsoft", connection!.ProviderKey);
|
|
Assert.Equal("user@outlook.test", connection.Address);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetConnectionAsync_returns_null_when_not_connected()
|
|
{
|
|
var graph = new Mock<IMicrosoftGraphOAuthService>();
|
|
graph.Setup(service => service.GetConnectionAsync("user-1", It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync((JobTrackerApi.Models.MicrosoftGraphConnection?)null);
|
|
|
|
var provider = new MicrosoftGraphProvider(graph.Object);
|
|
var connection = await provider.GetConnectionAsync("user-1", CancellationToken.None);
|
|
|
|
Assert.Null(connection);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SearchAsync_maps_conversation_id_onto_neutral_thread_id()
|
|
{
|
|
var graph = new Mock<IMicrosoftGraphOAuthService>();
|
|
graph.Setup(service => service.ListMessagesAsync("user-1", "recruiter", 10, It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new List<MicrosoftGraphMessageSummary>
|
|
{
|
|
new("msg-1", "conv-1", "Interview", "them@company.test", "me@outlook.test", DateTimeOffset.UtcNow, "snippet")
|
|
});
|
|
|
|
var provider = new MicrosoftGraphProvider(graph.Object);
|
|
var results = await provider.SearchAsync("user-1", "recruiter", 10, CancellationToken.None);
|
|
|
|
var summary = Assert.Single(results);
|
|
Assert.Equal("msg-1", summary.Id);
|
|
Assert.Equal("conv-1", summary.ThreadId);
|
|
Assert.Equal("Interview", summary.Subject);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetMessageAsync_maps_attachment_id_onto_neutral_external_attachment_id()
|
|
{
|
|
var graph = new Mock<IMicrosoftGraphOAuthService>();
|
|
graph.Setup(service => service.GetMessageAsync("user-1", "msg-1", It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new MicrosoftGraphMessageDetail(
|
|
"msg-1", "conv-1", "Offer", "them@company.test", "me@outlook.test", DateTimeOffset.UtcNow, "snippet",
|
|
"body text", "<p>body</p>", new List<string> { "Inbox" },
|
|
new List<MicrosoftGraphMessageAttachment> { new("resume.pdf", "application/pdf", 1024, "graph-att-1", false) }));
|
|
|
|
var provider = new MicrosoftGraphProvider(graph.Object);
|
|
var detail = await provider.GetMessageAsync("user-1", "msg-1", CancellationToken.None);
|
|
|
|
Assert.Equal("conv-1", detail.ThreadId);
|
|
var attachment = Assert.Single(detail.Attachments);
|
|
Assert.Equal("resume.pdf", attachment.FileName);
|
|
Assert.Equal("graph-att-1", attachment.ExternalAttachmentId);
|
|
}
|
|
}
|