using System.Security.Claims; using JobTrackerApi.Controllers; using JobTrackerApi.Services.EmailProviders; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging.Abstractions; using JobTrackerApi.Services; using JobTrackerApi.Tests.TestSupport; using Xunit; namespace JobTrackerApi.Tests; public sealed class EmailControllerTests { [Fact] public async Task Provider_status_is_owner_scoped_and_does_not_claim_send_support() { var gmail = new FakeProvider("gmail", new EmailConnectionInfo("gmail", "owner@gmail.test", CanSend: true)); var outlook = new FakeProvider("microsoft", null); var controller = CreateController(gmail, outlook); var result = await controller.GetProviders(CancellationToken.None); var ok = Assert.IsType(result.Result); var statuses = Assert.IsAssignableFrom>(ok.Value); Assert.Equal("user-1", gmail.LastOwnerUserId); Assert.Collection(statuses, status => { Assert.Equal("Gmail", status.DisplayName); Assert.True(status.Connected); Assert.True(status.CanRead); Assert.True(status.CanSend); }, status => { Assert.Equal("Outlook", status.DisplayName); Assert.False(status.Connected); Assert.False(status.CanRead); Assert.False(status.CanSend); }); } [Fact] public async Task Search_rejects_unknown_or_disconnected_provider() { var controller = CreateController(new FakeProvider("gmail", null)); var unknown = await controller.Search("unknown", null, 25, CancellationToken.None); Assert.IsType(unknown.Result); var disconnected = await controller.Search("gmail", null, 25, CancellationToken.None); Assert.IsType(disconnected.Result); } [Fact] public async Task Message_detail_returns_plain_text_without_provider_html() { var provider = new FakeProvider("gmail", new EmailConnectionInfo("gmail", "owner@gmail.test")); var controller = CreateController(provider); var result = await controller.GetMessage("gmail", "message-1", CancellationToken.None); var ok = Assert.IsType(result.Result); var detail = Assert.IsType(ok.Value); Assert.Equal("Safe plain text", detail.BodyText); Assert.DoesNotContain("script", System.Text.Json.JsonSerializer.Serialize(detail), StringComparison.OrdinalIgnoreCase); Assert.Equal("user-1", provider.LastOwnerUserId); } private static EmailController CreateController(params IEmailProvider[] providers) { var db = TestHostFactory.CreateInMemoryDb(); var controller = new EmailController(new EmailProviderRegistry(providers), db, new EmailSendAttemptStore(db, TimeProvider.System), NullLogger.Instance); controller.ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(new ClaimsIdentity( new[] { new Claim(ClaimTypes.NameIdentifier, "user-1") }, "test")) } }; return controller; } private sealed class FakeProvider(string providerKey, EmailConnectionInfo? connection) : IEmailProvider { public string ProviderKey => providerKey; public string? LastOwnerUserId { get; private set; } public Task GetConnectionAsync(string ownerUserId, CancellationToken cancellationToken) { LastOwnerUserId = ownerUserId; return Task.FromResult(connection); } public Task> SearchAsync(string ownerUserId, string? query, int maxResults, CancellationToken cancellationToken) => Task.FromResult>(Array.Empty()); public Task> ListThreadMessagesAsync(string ownerUserId, string threadId, CancellationToken cancellationToken) => Task.FromResult>(Array.Empty()); public Task GetMessageAsync(string ownerUserId, string messageId, CancellationToken cancellationToken) { LastOwnerUserId = ownerUserId; return Task.FromResult(new EmailMessageDetail( messageId, "thread-1", "Interview", "recruiter@example.test", "owner@gmail.test", DateTimeOffset.UtcNow, "Snippet", "Safe plain text", "", Array.Empty(), Array.Empty())); } public Task SendAsync(string ownerUserId, EmailDeliveryRequest request, CancellationToken cancellationToken) => Task.FromResult(new EmailDeliveryResult("message-1", "thread-1")); } }