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.
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
using System.Security.Claims;
|
||||
using JdeScoping.Api.Controllers;
|
||||
using JdeScoping.Api.Models;
|
||||
using JdeScoping.Core.Interfaces;
|
||||
using JdeScoping.Core.Models;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Shouldly;
|
||||
|
||||
namespace JdeScoping.Api.Tests.Controllers;
|
||||
|
||||
public class AuthControllerTests
|
||||
{
|
||||
private readonly IAuthService _authService;
|
||||
private readonly ILogger<AuthController> _logger;
|
||||
private readonly AuthController _controller;
|
||||
|
||||
public AuthControllerTests()
|
||||
{
|
||||
_authService = Substitute.For<IAuthService>();
|
||||
_logger = Substitute.For<ILogger<AuthController>>();
|
||||
_controller = new AuthController(_authService, _logger);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Login_WithValidCredentials_ReturnsUserInfo()
|
||||
{
|
||||
// Arrange
|
||||
var request = new LoginRequest { Username = "testuser", Password = "password123" };
|
||||
var user = new UserInfo
|
||||
{
|
||||
Dn = "CN=testuser,DC=example,DC=com",
|
||||
Username = "testuser",
|
||||
FirstName = "Test",
|
||||
LastName = "User",
|
||||
EmailAddress = "test@example.com",
|
||||
Title = "Developer"
|
||||
};
|
||||
_authService.AuthenticateAsync("testuser", "password123", Arg.Any<CancellationToken>())
|
||||
.Returns(new AuthResult(true, user, null));
|
||||
|
||||
// Setup HttpContext with mock authentication
|
||||
var httpContext = CreateMockHttpContext();
|
||||
_controller.ControllerContext = new ControllerContext { HttpContext = httpContext };
|
||||
|
||||
// Act
|
||||
var result = await _controller.Login(request, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Result.ShouldBeOfType<OkObjectResult>();
|
||||
var okResult = (OkObjectResult)result.Result!;
|
||||
var returnedUser = okResult.Value.ShouldBeOfType<UserInfo>();
|
||||
returnedUser.Username.ShouldBe("testuser");
|
||||
returnedUser.FirstName.ShouldBe("Test");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Login_WithInvalidCredentials_Returns401()
|
||||
{
|
||||
// Arrange
|
||||
var request = new LoginRequest { Username = "testuser", Password = "wrongpassword" };
|
||||
_authService.AuthenticateAsync("testuser", "wrongpassword", Arg.Any<CancellationToken>())
|
||||
.Returns(new AuthResult(false, null, "Incorrect username or password"));
|
||||
|
||||
// Setup HttpContext
|
||||
var httpContext = CreateMockHttpContext();
|
||||
_controller.ControllerContext = new ControllerContext { HttpContext = httpContext };
|
||||
|
||||
// Act
|
||||
var result = await _controller.Login(request, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Result.ShouldBeOfType<UnauthorizedObjectResult>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Logout_ClearsAuthentication()
|
||||
{
|
||||
// Arrange
|
||||
var httpContext = CreateAuthenticatedHttpContext("testuser");
|
||||
_controller.ControllerContext = new ControllerContext { HttpContext = httpContext };
|
||||
|
||||
// Act
|
||||
var result = await _controller.Logout();
|
||||
|
||||
// Assert
|
||||
result.ShouldBeOfType<OkResult>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetCurrentUser_WhenAuthenticated_ReturnsUserInfo()
|
||||
{
|
||||
// Arrange
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(ClaimTypes.Name, "testuser"),
|
||||
new(ClaimTypes.GivenName, "Test"),
|
||||
new(ClaimTypes.Surname, "User"),
|
||||
new(ClaimTypes.Email, "test@example.com"),
|
||||
new("title", "Developer"),
|
||||
new("dn", "CN=testuser,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 };
|
||||
|
||||
// Act
|
||||
var result = _controller.GetCurrentUser();
|
||||
|
||||
// Assert
|
||||
result.Result.ShouldBeOfType<OkObjectResult>();
|
||||
var okResult = (OkObjectResult)result.Result!;
|
||||
var user = okResult.Value.ShouldBeOfType<UserInfo>();
|
||||
user.Username.ShouldBe("testuser");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetCurrentUser_ExtractsAllClaimsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(ClaimTypes.Name, "jsmith"),
|
||||
new(ClaimTypes.GivenName, "John"),
|
||||
new(ClaimTypes.Surname, "Smith"),
|
||||
new(ClaimTypes.Email, "jsmith@example.com"),
|
||||
new("title", "Senior Engineer"),
|
||||
new("dn", "CN=jsmith,OU=Users,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 };
|
||||
|
||||
// Act
|
||||
var result = _controller.GetCurrentUser();
|
||||
|
||||
// Assert
|
||||
result.Result.ShouldBeOfType<OkObjectResult>();
|
||||
var okResult = (OkObjectResult)result.Result!;
|
||||
var user = okResult.Value.ShouldBeOfType<UserInfo>();
|
||||
|
||||
user.Username.ShouldBe("jsmith");
|
||||
user.FirstName.ShouldBe("John");
|
||||
user.LastName.ShouldBe("Smith");
|
||||
user.EmailAddress.ShouldBe("jsmith@example.com");
|
||||
user.Title.ShouldBe("Senior Engineer");
|
||||
user.Dn.ShouldBe("CN=jsmith,OU=Users,DC=example,DC=com");
|
||||
user.DisplayName.ShouldBe("John Smith");
|
||||
}
|
||||
|
||||
private static HttpContext CreateMockHttpContext()
|
||||
{
|
||||
var authServiceMock = Substitute.For<IAuthenticationService>();
|
||||
authServiceMock.SignOutAsync(Arg.Any<HttpContext>(), Arg.Any<string>(), Arg.Any<AuthenticationProperties>())
|
||||
.Returns(Task.CompletedTask);
|
||||
authServiceMock.SignInAsync(Arg.Any<HttpContext>(), Arg.Any<string>(), Arg.Any<ClaimsPrincipal>(), Arg.Any<AuthenticationProperties>())
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
var serviceProvider = Substitute.For<IServiceProvider>();
|
||||
serviceProvider.GetService(typeof(IAuthenticationService)).Returns(authServiceMock);
|
||||
|
||||
var httpContext = new DefaultHttpContext
|
||||
{
|
||||
RequestServices = serviceProvider
|
||||
};
|
||||
|
||||
return httpContext;
|
||||
}
|
||||
|
||||
private static HttpContext CreateAuthenticatedHttpContext(string username)
|
||||
{
|
||||
var httpContext = CreateMockHttpContext();
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(ClaimTypes.Name, username),
|
||||
new("dn", $"CN={username},DC=example,DC=com")
|
||||
};
|
||||
var identity = new ClaimsIdentity(claims, "Test");
|
||||
httpContext.User = new ClaimsPrincipal(identity);
|
||||
return httpContext;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
using JdeScoping.Api.Controllers;
|
||||
using JdeScoping.Api.Models;
|
||||
using JdeScoping.Core.Interfaces;
|
||||
using JdeScoping.Core.Models.Inventory;
|
||||
using JdeScoping.Core.Models.WorkOrders;
|
||||
using JdeScoping.Core.ViewModels;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Shouldly;
|
||||
|
||||
namespace JdeScoping.Api.Tests.Controllers;
|
||||
|
||||
public class FileControllerTests
|
||||
{
|
||||
private readonly ILotFinderRepository _repository;
|
||||
private readonly IExcelParserService _parserService;
|
||||
private readonly IExcelTemplateService _templateService;
|
||||
private readonly ILogger<FileIOController> _logger;
|
||||
private readonly FileIOController _controller;
|
||||
|
||||
public FileControllerTests()
|
||||
{
|
||||
_repository = Substitute.For<ILotFinderRepository>();
|
||||
_parserService = Substitute.For<IExcelParserService>();
|
||||
_templateService = Substitute.For<IExcelTemplateService>();
|
||||
_logger = Substitute.For<ILogger<FileIOController>>();
|
||||
_controller = new FileIOController(_repository, _parserService, _templateService, _logger);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UploadWorkOrders_CallsParserAndRepository()
|
||||
{
|
||||
// Arrange
|
||||
var formFile = CreateFormFile(new byte[] { 1, 2, 3 }, "workorders.xlsx");
|
||||
var parsedNumbers = new List<long> { 12345, 67890 };
|
||||
_parserService.ParseWorkOrders(Arg.Any<Stream>()).Returns(parsedNumbers);
|
||||
|
||||
var workOrders = new List<WorkOrder>
|
||||
{
|
||||
new() { WorkOrderNumber = 12345, ItemNumber = "ITEM-001" },
|
||||
new() { WorkOrderNumber = 67890, ItemNumber = "ITEM-002" }
|
||||
};
|
||||
_repository.LookupWorkordersAsync(parsedNumbers, Arg.Any<CancellationToken>())
|
||||
.Returns(workOrders);
|
||||
|
||||
// Act
|
||||
var result = await _controller.UploadWorkOrders(formFile, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Result.ShouldBeOfType<OkObjectResult>();
|
||||
var okResult = (OkObjectResult)result.Result!;
|
||||
var uploadResult = okResult.Value.ShouldBeOfType<FileUploadResult<WorkOrderViewModel>>();
|
||||
uploadResult.WasSuccessful.ShouldBeTrue();
|
||||
uploadResult.Data.ShouldNotBeNull();
|
||||
uploadResult.Data.Length.ShouldBe(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DownloadWorkOrders_CallsTemplateService()
|
||||
{
|
||||
// Arrange
|
||||
var workOrders = new List<long> { 12345, 67890 };
|
||||
var expectedBytes = new byte[] { 1, 2, 3, 4, 5 };
|
||||
_templateService.GenerateSingleColumn(workOrders, "Work Order Number").Returns(expectedBytes);
|
||||
|
||||
// Act
|
||||
var result = _controller.DownloadWorkOrders(workOrders);
|
||||
|
||||
// Assert
|
||||
result.ShouldBeOfType<FileContentResult>();
|
||||
var fileResult = (FileContentResult)result;
|
||||
fileResult.ContentType.ShouldBe("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||
fileResult.FileDownloadName.ShouldBe("work_order_template.xlsx");
|
||||
fileResult.FileContents.ShouldBe(expectedBytes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UploadWorkOrders_NoFile_ReturnsError()
|
||||
{
|
||||
// Act
|
||||
var result = await _controller.UploadWorkOrders(null, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Result.ShouldBeOfType<OkObjectResult>();
|
||||
var okResult = (OkObjectResult)result.Result!;
|
||||
var uploadResult = okResult.Value.ShouldBeOfType<FileUploadResult<WorkOrderViewModel>>();
|
||||
uploadResult.WasSuccessful.ShouldBeFalse();
|
||||
uploadResult.ErrorMessage.ShouldBe("No file uploaded");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UploadPartOperations_CallsParser()
|
||||
{
|
||||
// Arrange
|
||||
var formFile = CreateFormFile(new byte[] { 1, 2, 3 }, "partops.xlsx");
|
||||
var parsedOps = new List<PartOperationViewModel>
|
||||
{
|
||||
new() { ItemNumber = "ITEM-001", OperationNumber = "100", MisNumber = "MIS001", MisRevision = "A" }
|
||||
};
|
||||
_parserService.ParsePartOperations(Arg.Any<Stream>()).Returns(parsedOps);
|
||||
|
||||
// Act
|
||||
var result = _controller.UploadPartOperations(formFile);
|
||||
|
||||
// Assert
|
||||
result.Result.ShouldBeOfType<OkObjectResult>();
|
||||
var okResult = (OkObjectResult)result.Result!;
|
||||
var uploadResult = okResult.Value.ShouldBeOfType<FileUploadResult<PartOperationViewModel>>();
|
||||
uploadResult.WasSuccessful.ShouldBeTrue();
|
||||
uploadResult.Data.ShouldNotBeNull();
|
||||
uploadResult.Data.Length.ShouldBe(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UploadComponentLots_ParsesTwoColumnsAndLooksUpLots()
|
||||
{
|
||||
// Arrange
|
||||
var formFile = CreateFormFile(new byte[] { 1, 2, 3 }, "componentlots.xlsx");
|
||||
var parsedLots = new List<LotViewModel>
|
||||
{
|
||||
new() { LotNumber = "LOT001", ItemNumber = "ITEM-001" },
|
||||
new() { LotNumber = "LOT002", ItemNumber = "ITEM-002" }
|
||||
};
|
||||
_parserService.ParseComponentLots(Arg.Any<Stream>()).Returns(parsedLots);
|
||||
|
||||
var lots = new List<Lot>
|
||||
{
|
||||
new() { LotNumber = "LOT001", ItemNumber = "ITEM-001" },
|
||||
new() { LotNumber = "LOT002", ItemNumber = "ITEM-002" }
|
||||
};
|
||||
_repository.LookupLotsAsync(Arg.Any<List<LotViewModel>>(), Arg.Any<CancellationToken>())
|
||||
.Returns(lots);
|
||||
|
||||
// Act
|
||||
var result = await _controller.UploadComponentLots(formFile, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Result.ShouldBeOfType<OkObjectResult>();
|
||||
var okResult = (OkObjectResult)result.Result!;
|
||||
var uploadResult = okResult.Value.ShouldBeOfType<FileUploadResult<LotViewModel>>();
|
||||
uploadResult.WasSuccessful.ShouldBeTrue();
|
||||
uploadResult.Data.ShouldNotBeNull();
|
||||
uploadResult.Data.Length.ShouldBe(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DownloadComponentLots_CallsTemplateService()
|
||||
{
|
||||
// Arrange
|
||||
var lots = new List<LotViewModel>
|
||||
{
|
||||
new() { LotNumber = "LOT001", ItemNumber = "ITEM-001" },
|
||||
new() { LotNumber = "LOT002", ItemNumber = "ITEM-002" }
|
||||
};
|
||||
var expectedBytes = new byte[] { 1, 2, 3, 4, 5 };
|
||||
_templateService.GenerateMultiColumn(
|
||||
Arg.Any<object?[][]>(),
|
||||
Arg.Is<string[]>(h => h.Contains("Component Lot Number") && h.Contains("Component Item Number")))
|
||||
.Returns(expectedBytes);
|
||||
|
||||
// Act
|
||||
var result = _controller.DownloadComponentLots(lots);
|
||||
|
||||
// Assert
|
||||
result.ShouldBeOfType<FileContentResult>();
|
||||
var fileResult = (FileContentResult)result;
|
||||
fileResult.ContentType.ShouldBe("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||
fileResult.FileDownloadName.ShouldBe("component_lot_template.xlsx");
|
||||
fileResult.FileContents.ShouldBe(expectedBytes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UploadItems_CallsParserAndRepository()
|
||||
{
|
||||
// Arrange
|
||||
var formFile = CreateFormFile(new byte[] { 1, 2, 3 }, "items.xlsx");
|
||||
var parsedNumbers = new List<string> { "ITEM-001", "ITEM-002" };
|
||||
_parserService.ParseItems(Arg.Any<Stream>()).Returns(parsedNumbers);
|
||||
|
||||
var items = new List<Item>
|
||||
{
|
||||
new() { ItemNumber = "ITEM-001", Description = "Item 1" },
|
||||
new() { ItemNumber = "ITEM-002", Description = "Item 2" }
|
||||
};
|
||||
_repository.LookupItemsAsync(parsedNumbers, Arg.Any<CancellationToken>())
|
||||
.Returns(items);
|
||||
|
||||
// Act
|
||||
var result = await _controller.UploadItems(formFile, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Result.ShouldBeOfType<OkObjectResult>();
|
||||
var okResult = (OkObjectResult)result.Result!;
|
||||
var uploadResult = okResult.Value.ShouldBeOfType<FileUploadResult<ItemViewModel>>();
|
||||
uploadResult.WasSuccessful.ShouldBeTrue();
|
||||
uploadResult.Data.ShouldNotBeNull();
|
||||
uploadResult.Data.Length.ShouldBe(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DownloadItems_CallsTemplateService()
|
||||
{
|
||||
// Arrange
|
||||
var items = new List<ItemViewModel>
|
||||
{
|
||||
new() { ItemNumber = "ITEM-001", Description = "Item 1" },
|
||||
new() { ItemNumber = "ITEM-002", Description = "Item 2" }
|
||||
};
|
||||
var expectedBytes = new byte[] { 1, 2, 3, 4, 5 };
|
||||
_templateService.GenerateMultiColumn(
|
||||
Arg.Any<object?[][]>(),
|
||||
Arg.Is<string[]>(h => h.Contains("Item Number")))
|
||||
.Returns(expectedBytes);
|
||||
|
||||
// Act
|
||||
var result = _controller.DownloadItems(items);
|
||||
|
||||
// Assert
|
||||
result.ShouldBeOfType<FileContentResult>();
|
||||
var fileResult = (FileContentResult)result;
|
||||
fileResult.ContentType.ShouldBe("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||
fileResult.FileDownloadName.ShouldBe("item_number_template.xlsx");
|
||||
fileResult.FileContents.ShouldBe(expectedBytes);
|
||||
}
|
||||
|
||||
private static IFormFile CreateFormFile(byte[] content, string fileName)
|
||||
{
|
||||
var stream = new MemoryStream(content);
|
||||
var formFile = Substitute.For<IFormFile>();
|
||||
formFile.OpenReadStream().Returns(stream);
|
||||
formFile.FileName.Returns(fileName);
|
||||
formFile.Length.Returns(content.Length);
|
||||
return formFile;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
using JdeScoping.Api.Controllers;
|
||||
using JdeScoping.Core.Interfaces;
|
||||
using JdeScoping.Core.Models;
|
||||
using JdeScoping.Core.Models.Inventory;
|
||||
using JdeScoping.Core.Models.Organization;
|
||||
using JdeScoping.Core.ViewModels;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NSubstitute;
|
||||
using Shouldly;
|
||||
|
||||
namespace JdeScoping.Api.Tests.Controllers;
|
||||
|
||||
public class LookupControllerTests
|
||||
{
|
||||
private readonly ILotFinderRepository _repository;
|
||||
private readonly LookupController _controller;
|
||||
|
||||
public LookupControllerTests()
|
||||
{
|
||||
_repository = Substitute.For<ILotFinderRepository>();
|
||||
_controller = new LookupController(_repository);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindItems_ReturnsOrderedResults()
|
||||
{
|
||||
// Arrange
|
||||
var items = new List<Item>
|
||||
{
|
||||
new Item { ItemNumber = "ITEM-002", Description = "Second Item" },
|
||||
new Item { ItemNumber = "ITEM-001", Description = "First Item" }
|
||||
};
|
||||
_repository.SearchItemsAsync("ITEM", Arg.Any<CancellationToken>())
|
||||
.Returns(items);
|
||||
|
||||
// Act
|
||||
var result = await _controller.FindItems("ITEM", CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Result.ShouldBeOfType<OkObjectResult>();
|
||||
var okResult = (OkObjectResult)result.Result!;
|
||||
var viewModels = okResult.Value.ShouldBeAssignableTo<IEnumerable<ItemViewModel>>()!.ToList();
|
||||
viewModels.Count.ShouldBe(2);
|
||||
viewModels[0].ItemNumber.ShouldBe("ITEM-001"); // Ordered by ItemNumber
|
||||
viewModels[1].ItemNumber.ShouldBe("ITEM-002");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindProfitCenters_ReturnsOrderedResults()
|
||||
{
|
||||
// Arrange
|
||||
var centers = new List<ProfitCenter>
|
||||
{
|
||||
new ProfitCenter { Code = "PC2", Description = "Center 2" },
|
||||
new ProfitCenter { Code = "PC1", Description = "Center 1" }
|
||||
};
|
||||
_repository.SearchProfitCentersAsync("PC", Arg.Any<CancellationToken>())
|
||||
.Returns(centers);
|
||||
|
||||
// Act
|
||||
var result = await _controller.FindProfitCenters("PC", CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Result.ShouldBeOfType<OkObjectResult>();
|
||||
var okResult = (OkObjectResult)result.Result!;
|
||||
var viewModels = okResult.Value.ShouldBeAssignableTo<IEnumerable<ProfitCenterViewModel>>()!.ToList();
|
||||
viewModels.Count.ShouldBe(2);
|
||||
viewModels[0].Code.ShouldBe("PC1"); // Ordered by Code
|
||||
viewModels[1].Code.ShouldBe("PC2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindWorkCenters_ReturnsOrderedResults()
|
||||
{
|
||||
// Arrange
|
||||
var centers = new List<WorkCenter>
|
||||
{
|
||||
new WorkCenter { Code = "WC2", Description = "Center 2" },
|
||||
new WorkCenter { Code = "WC1", Description = "Center 1" }
|
||||
};
|
||||
_repository.SearchWorkCentersAsync("WC", Arg.Any<CancellationToken>())
|
||||
.Returns(centers);
|
||||
|
||||
// Act
|
||||
var result = await _controller.FindWorkCenters("WC", CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Result.ShouldBeOfType<OkObjectResult>();
|
||||
var okResult = (OkObjectResult)result.Result!;
|
||||
var viewModels = okResult.Value.ShouldBeAssignableTo<IEnumerable<WorkCenterViewModel>>()!.ToList();
|
||||
viewModels.Count.ShouldBe(2);
|
||||
viewModels[0].Code.ShouldBe("WC1"); // Ordered by Code
|
||||
viewModels[1].Code.ShouldBe("WC2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindOperators_ReturnsOrderedResults()
|
||||
{
|
||||
// Arrange
|
||||
var users = new List<JdeUser>
|
||||
{
|
||||
new JdeUser { AddressNumber = 1, UserId = "user1", FullName = "Zebra, Alice" },
|
||||
new JdeUser { AddressNumber = 2, UserId = "user2", FullName = "Adams, Bob" }
|
||||
};
|
||||
_repository.SearchUsersAsync("user", Arg.Any<CancellationToken>())
|
||||
.Returns(users);
|
||||
|
||||
// Act
|
||||
var result = await _controller.FindOperators("user", CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Result.ShouldBeOfType<OkObjectResult>();
|
||||
var okResult = (OkObjectResult)result.Result!;
|
||||
var viewModels = okResult.Value.ShouldBeAssignableTo<IEnumerable<JdeUserViewModel>>()!.ToList();
|
||||
viewModels.Count.ShouldBe(2);
|
||||
viewModels[0].FullName.ShouldBe("Adams, Bob"); // Ordered by FullName
|
||||
viewModels[1].FullName.ShouldBe("Zebra, Alice");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindItems_WithNullQuery_PassesEmptyString()
|
||||
{
|
||||
// Arrange
|
||||
_repository.SearchItemsAsync(string.Empty, Arg.Any<CancellationToken>())
|
||||
.Returns(new List<Item>());
|
||||
|
||||
// Act
|
||||
var result = await _controller.FindItems(null!, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Result.ShouldBeOfType<OkObjectResult>();
|
||||
await _repository.Received(1).SearchItemsAsync(string.Empty, Arg.Any<CancellationToken>());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
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 };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user