using System.Security.Claims; using JobTrackerApi.Controllers; using JobTrackerApi.Data; using JobTrackerApi.Models; using JobTrackerApi.Services; using JobTrackerApi.Tests.TestSupport; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Moq; using Xunit; namespace JobTrackerApi.Tests; public sealed class MicrosoftGraphControllerTests { [Fact] public async Task Status_returns_sync_state_fields_for_connected_account() { var graph = new Mock(); graph.Setup(service => service.GetConnectionAsync("user-1", It.IsAny())) .ReturnsAsync(new MicrosoftGraphConnection { OwnerUserId = "user-1", MailAddress = "user@outlook.test", ConnectedAt = DateTimeOffset.UtcNow.AddDays(-2), LastSyncedAt = DateTimeOffset.UtcNow.AddMinutes(-10), LastSyncAttemptedAt = DateTimeOffset.UtcNow.AddMinutes(-5), LastSyncSucceededAt = DateTimeOffset.UtcNow.AddMinutes(-10), LastSyncMode = "list-messages", LastSyncSource = "custom-query", LastSyncStatus = "error", LastSyncError = "Token refresh failed" }); var controller = CreateController(graph.Object, "user-1"); var result = await controller.Status(CancellationToken.None); var ok = Assert.IsType(result.Result); var payload = Assert.IsType(ok.Value); Assert.True(payload.Connected); Assert.Equal("user@outlook.test", payload.MailAddress); Assert.Equal("list-messages", payload.LastSyncMode); Assert.Equal("custom-query", payload.LastSyncSource); Assert.Equal("error", payload.LastSyncStatus); Assert.Equal("Token refresh failed", payload.LastSyncError); } [Fact] public async Task Status_reports_not_connected_when_no_connection_exists() { var graph = new Mock(); graph.Setup(service => service.GetConnectionAsync("user-1", It.IsAny())) .ReturnsAsync((MicrosoftGraphConnection?)null); var controller = CreateController(graph.Object, "user-1"); var result = await controller.Status(CancellationToken.None); var ok = Assert.IsType(result.Result); var payload = Assert.IsType(ok.Value); Assert.False(payload.Connected); Assert.Null(payload.MailAddress); } [Fact] public void ConnectUrl_returns_authorization_url_from_service() { var graph = new Mock(); graph.Setup(service => service.BuildAuthorizationUrl("user-1", It.IsAny())) .Returns("https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=test"); var controller = CreateController(graph.Object, "user-1"); var result = controller.ConnectUrl(); var ok = Assert.IsType(result); var url = ok.Value!.GetType().GetProperty("url")!.GetValue(ok.Value) as string; Assert.Equal("https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=test", url); } [Fact] public async Task Callback_returns_error_html_when_state_is_invalid() { var graph = new Mock(); graph.Setup(service => service.ConsumeState("bad-state")).Returns((string?)null); var controller = CreateController(graph.Object, "user-1"); var result = await controller.Callback("auth-code", "bad-state", null, CancellationToken.None); var content = Assert.IsType(result); Assert.Contains("no longer valid", content.Content); graph.Verify(service => service.ExchangeCodeAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Fact] public async Task Callback_returns_error_html_when_provider_returns_error() { var graph = new Mock(MockBehavior.Strict); var controller = CreateController(graph.Object, "user-1"); var result = await controller.Callback(null, null, "access_denied", CancellationToken.None); var content = Assert.IsType(result); Assert.Contains("access_denied", content.Content); } [Fact] public async Task Callback_exchanges_code_and_reports_connected_mail_address() { var graph = new Mock(); graph.Setup(service => service.ConsumeState("good-state")).Returns("user-1"); graph.Setup(service => service.ExchangeCodeAsync("user-1", "auth-code", It.IsAny(), It.IsAny())) .ReturnsAsync(new MicrosoftGraphOAuthExchangeResult("user@outlook.test")); var controller = CreateController(graph.Object, "user-1"); var result = await controller.Callback("auth-code", "good-state", null, CancellationToken.None); var content = Assert.IsType(result); Assert.Contains("user@outlook.test", content.Content); } [Fact] public async Task Disconnect_calls_service_for_authenticated_user() { var graph = new Mock(); graph.Setup(service => service.DisconnectAsync("user-1", It.IsAny())).Returns(Task.CompletedTask); var controller = CreateController(graph.Object, "user-1"); var result = await controller.Disconnect(CancellationToken.None); Assert.IsType(result); graph.Verify(service => service.DisconnectAsync("user-1", It.IsAny()), Times.Once); } private static MicrosoftGraphController CreateController(IMicrosoftGraphOAuthService graph, string userId) { return new MicrosoftGraphController(graph, BuildConfig()) { ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, userId) }, "test")) } } }; } private static IConfiguration BuildConfig() { return new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary()) .Build(); } }