using System.Security.Claims; using JdeScoping.Api.Controllers; using JdeScoping.Api.Hubs; using JdeScoping.Core.Interfaces; using JdeScoping.Core.Models; using JdeScoping.Core.Models.Enums; using JdeScoping.Core.Models.Search; using JdeScoping.Core.ViewModels; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Logging; using NSubstitute; using Shouldly; namespace JdeScoping.Api.Tests.Controllers; public class SearchControllerTests { private readonly ILotFinderRepository _repository; private readonly IHubContext _hubContext; private readonly ILogger _logger; private readonly TimeProvider _timeProvider; private readonly SearchController _controller; public SearchControllerTests() { _repository = Substitute.For(); _hubContext = Substitute.For>(); _logger = Substitute.For>(); _timeProvider = TimeProvider.System; _controller = new SearchController(_repository, _hubContext, _logger, _timeProvider); SetupAuthenticatedUser("testuser"); } [Fact] public async Task GetSearches_ReturnsUserSearches_OrderedByStartDtDescending() { // Arrange var searches = new List { new Search { Id = 1, Name = "Search 1", UserName = "testuser", StartDt = DateTime.Now.AddHours(-2) }, new Search { Id = 2, Name = "Search 2", UserName = "testuser", StartDt = DateTime.Now } }; _repository.GetUserSearchesAsync("testuser", Arg.Any()) .Returns(searches); // Act var result = await _controller.GetSearches(CancellationToken.None); // Assert result.Result.ShouldBeOfType(); var okResult = (OkObjectResult)result.Result!; var viewModels = okResult.Value.ShouldBeAssignableTo>()!.ToList(); viewModels.Count.ShouldBe(2); viewModels[0].Id.ShouldBe(2); // Most recent first viewModels[1].Id.ShouldBe(1); } [Fact] public async Task CreateSearch_SavesAndPublishesToSignalR() { // Arrange var viewModel = new SearchViewModel { Name = "New Search", Status = SearchStatus.New, Criteria = new SearchCriteria() }; _repository.SubmitSearchAsync(Arg.Any(), Arg.Any()) .Returns(42); var clientProxy = Substitute.For(); var hubClients = Substitute.For(); hubClients.All.Returns(clientProxy); _hubContext.Clients.Returns(hubClients); // Act var result = await _controller.CreateSearch(viewModel, CancellationToken.None); // Assert result.Result.ShouldBeOfType(); var createdResult = (CreatedAtActionResult)result.Result!; createdResult.Value.ShouldBe(42); await _repository.Received(1).SubmitSearchAsync( Arg.Is(s => s.Name == "New Search" && s.UserName == "testuser"), Arg.Any()); } [Fact] public async Task CopySearch_ResetsStatusAndTimestamps() { // Arrange var original = new Search { Id = 1, Name = "Original Search", UserName = "otheruser", Status = SearchStatus.Ended, SubmitDt = DateTime.Now.AddHours(-2), StartDt = DateTime.Now.AddHours(-2), EndDt = DateTime.Now.AddHours(-1), CriteriaJson = "{}" }; _repository.GetSearchAsync(1, Arg.Any()) .Returns(original); _repository.SubmitSearchAsync(Arg.Any(), Arg.Any()) .Returns(42); // Act var result = await _controller.CopySearch(1, CancellationToken.None); // Assert - CopySearch returns Ok with a copy, doesn't persist var okResult = result.Result.ShouldBeOfType(); var viewModel = okResult.Value.ShouldBeOfType(); viewModel.Status.ShouldBe(SearchStatus.New); viewModel.UserName.ShouldBe("testuser"); viewModel.Name.ShouldBe("Original Search"); viewModel.SubmitDt.ShouldBeNull(); viewModel.StartDt.ShouldBeNull(); viewModel.EndDt.ShouldBeNull(); // CopySearch does NOT persist - it just returns a copy await _repository.DidNotReceive().SubmitSearchAsync( Arg.Is(s => s.Status == SearchStatus.New && s.UserName == "testuser" && s.SubmitDt == null && s.StartDt == null && s.EndDt == null && s.Name == "Original Search"), Arg.Any()); } [Fact] public async Task GetResults_ReturnsFileWithCorrectContentType() { // Arrange var excelData = new byte[] { 1, 2, 3, 4, 5 }; _repository.GetSearchResultsAsync(1, Arg.Any()) .Returns(excelData); // Act var result = await _controller.GetResults(1, CancellationToken.None); // Assert result.ShouldBeOfType(); var fileResult = (FileContentResult)result; fileResult.ContentType.ShouldBe("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); fileResult.FileDownloadName.ShouldBe("search_results.xlsx"); fileResult.FileContents.ShouldBe(excelData); } [Fact] public async Task GetResults_WhenNoResults_Returns404() { // Arrange _repository.GetSearchResultsAsync(1, Arg.Any()) .Returns((byte[]?)null); // Act var result = await _controller.GetResults(1, CancellationToken.None); // Assert result.ShouldBeOfType(); } [Fact] public async Task GetSearch_WhenNotFound_Returns404() { // Arrange _repository.GetSearchAsync(999, Arg.Any()) .Returns((Search?)null); // Act var result = await _controller.GetSearch(999, CancellationToken.None); // Assert result.Result.ShouldBeOfType(); } private void SetupAuthenticatedUser(string username) { var claims = new List { new(ClaimTypes.Name, username), new("dn", $"CN={username},DC=example,DC=com") }; var identity = new ClaimsIdentity(claims, "Test"); var principal = new ClaimsPrincipal(identity); var httpContext = new DefaultHttpContext { User = principal }; _controller.ControllerContext = new ControllerContext { HttpContext = httpContext }; } }