Files
jdescopingtool/NEW/tests/JdeScoping.Api.Tests/Controllers/SearchControllerTests.cs
T
Joseph Doherty 26ff8d9b4f Initial commit: JDE Scoping Tool migration project
Set up repository with legacy .NET Framework 4.8 source (OLD/),
new .NET 10 Blazor solution (NEW/), OpenSpec specifications,
documentation, and project configuration.
2026-01-02 07:43:29 -05:00

195 lines
6.9 KiB
C#

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<StatusHub> _hubContext;
private readonly ILogger<SearchController> _logger;
private readonly SearchController _controller;
public SearchControllerTests()
{
_repository = Substitute.For<ILotFinderRepository>();
_hubContext = Substitute.For<IHubContext<StatusHub>>();
_logger = Substitute.For<ILogger<SearchController>>();
_controller = new SearchController(_repository, _hubContext, _logger);
SetupAuthenticatedUser("testuser");
}
[Fact]
public async Task GetSearches_ReturnsUserSearches_OrderedByStartDtDescending()
{
// Arrange
var searches = new List<Search>
{
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<CancellationToken>())
.Returns(searches);
// Act
var result = await _controller.GetSearches(CancellationToken.None);
// Assert
result.Result.ShouldBeOfType<OkObjectResult>();
var okResult = (OkObjectResult)result.Result!;
var viewModels = okResult.Value.ShouldBeAssignableTo<IEnumerable<SearchViewModel>>()!.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<Search>(), Arg.Any<CancellationToken>())
.Returns(42);
var clientProxy = Substitute.For<IClientProxy>();
var hubClients = Substitute.For<IHubClients>();
hubClients.All.Returns(clientProxy);
_hubContext.Clients.Returns(hubClients);
// Act
var result = await _controller.CreateSearch(viewModel, CancellationToken.None);
// Assert
result.Result.ShouldBeOfType<CreatedAtActionResult>();
var createdResult = (CreatedAtActionResult)result.Result!;
createdResult.Value.ShouldBe(42);
await _repository.Received(1).SubmitSearchAsync(
Arg.Is<Search>(s => s.Name == "New Search" && s.UserName == "testuser"),
Arg.Any<CancellationToken>());
}
[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<CancellationToken>())
.Returns(original);
_repository.SubmitSearchAsync(Arg.Any<Search>(), Arg.Any<CancellationToken>())
.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<OkObjectResult>();
var viewModel = okResult.Value.ShouldBeOfType<SearchViewModel>();
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<Search>(s =>
s.Status == SearchStatus.New &&
s.UserName == "testuser" &&
s.SubmitDt == null &&
s.StartDt == null &&
s.EndDt == null &&
s.Name == "Original Search"),
Arg.Any<CancellationToken>());
}
[Fact]
public async Task GetResults_ReturnsFileWithCorrectContentType()
{
// Arrange
var excelData = new byte[] { 1, 2, 3, 4, 5 };
_repository.GetSearchResultsAsync(1, Arg.Any<CancellationToken>())
.Returns(excelData);
// Act
var result = await _controller.GetResults(1, CancellationToken.None);
// Assert
result.ShouldBeOfType<FileContentResult>();
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<CancellationToken>())
.Returns((byte[]?)null);
// Act
var result = await _controller.GetResults(1, CancellationToken.None);
// Assert
result.ShouldBeOfType<NotFoundResult>();
}
[Fact]
public async Task GetSearch_WhenNotFound_Returns404()
{
// Arrange
_repository.GetSearchAsync(999, Arg.Any<CancellationToken>())
.Returns((Search?)null);
// Act
var result = await _controller.GetSearch(999, CancellationToken.None);
// Assert
result.Result.ShouldBeOfType<NotFoundResult>();
}
private void SetupAuthenticatedUser(string username)
{
var claims = new List<Claim>
{
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 };
}
}