ec4c8fab87
Move configuration options from Core/DataAccess/DataSync/ExcelIO to dedicated Options folders within each project for better organization. Update all references and tests accordingly.
501 lines
19 KiB
C#
501 lines
19 KiB
C#
using ClosedXML.Excel;
|
|
using JdeScoping.ExcelIO.Options;
|
|
using JdeScoping.ExcelIO.Formatting;
|
|
using JdeScoping.ExcelIO.Generators;
|
|
using JdeScoping.ExcelIO.Helpers;
|
|
using JdeScoping.ExcelIO.Models.Reporting;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
using NSubstitute;
|
|
using Shouldly;
|
|
using Xunit;
|
|
|
|
namespace JdeScoping.ExcelIO.Tests;
|
|
|
|
/// <summary>
|
|
/// Tests comparing generated output against legacy format specifications.
|
|
/// These tests verify column order, format strings, and protection settings
|
|
/// match the legacy ExcelWriter.cs implementation.
|
|
/// </summary>
|
|
public class LegacyComparisonTests
|
|
{
|
|
private readonly ExcelExportService _service;
|
|
|
|
public LegacyComparisonTests()
|
|
{
|
|
var logger = Substitute.For<ILogger<ExcelExportService>>();
|
|
var options = Microsoft.Extensions.Options.Options.Create(new ExcelExportOptions
|
|
{
|
|
CriteriaSheetPassword = "JDE_SCOPING_TOOL_PASS",
|
|
DataSheetPassword = "JDESCOPINGTOOL"
|
|
});
|
|
|
|
var cache = new OutputColumnCache();
|
|
var tableWriter = new AttributeTableWriter(cache);
|
|
var criteriaGenerator = new CriteriaSheetGenerator(options, tableWriter);
|
|
|
|
_service = new ExcelExportService(logger, options, criteriaGenerator, tableWriter);
|
|
}
|
|
|
|
#region Search Results Column Order Tests
|
|
|
|
/// <summary>
|
|
/// Verifies Search Results columns match legacy order per ExcelWriter.cs lines 197-218.
|
|
/// Legacy order: Work Order Number, Work Order Branch Code, Lot Number, Item Number,
|
|
/// Planning Family, Order Quantity, Held Quantity, Scrapped Quantity, Shipped Quantity,
|
|
/// Operation Step Branch Code, Operation Step, Operation Step Description,
|
|
/// Function Operation Description, Operation Step Update Timestamp, Status Code,
|
|
/// Status Description, Status Update Timestamp, Inclusion Reason
|
|
///
|
|
/// Note: The new implementation adds "Stocking Type" after "Planning Family" per spec.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task SearchResults_ColumnOrder_MatchesLegacyWithEnhancements()
|
|
{
|
|
var search = CreateSearchModelWithResults();
|
|
var result = await _service.GenerateAsync(search);
|
|
|
|
using var stream = new MemoryStream(result);
|
|
using var workbook = new XLWorkbook(stream);
|
|
var sheet = workbook.Worksheet("Search Results");
|
|
|
|
// Verify key column positions (0-indexed from GetHeadersFromSheet)
|
|
var headers = GetHeadersFromSheet(sheet);
|
|
|
|
headers[0].ShouldBe("Work Order Number");
|
|
headers[1].ShouldBe("Work Order Branch Code");
|
|
headers[2].ShouldBe("Lot Number");
|
|
headers[3].ShouldBe("Item Number");
|
|
headers[4].ShouldBe("Planning Family");
|
|
// New column: Stocking Type (not in legacy)
|
|
headers[5].ShouldBe("Stocking Type");
|
|
headers[6].ShouldBe("Order Quantity");
|
|
headers[7].ShouldBe("Held Quantity");
|
|
headers[8].ShouldBe("Scrapped Quantity");
|
|
headers[9].ShouldBe("Shipped Quantity");
|
|
headers[10].ShouldBe("Operation Step Branch Code");
|
|
headers[11].ShouldBe("Operation Step");
|
|
headers[12].ShouldBe("Operation Step Description");
|
|
headers[13].ShouldBe("Function Operation Description");
|
|
headers[14].ShouldBe("Operation Step Update Timestamp");
|
|
headers[15].ShouldBe("Status Code");
|
|
headers[16].ShouldBe("Status Description");
|
|
headers[17].ShouldBe("Status Update Timestamp");
|
|
headers[18].ShouldBe("Inclusion Reason");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region MIS Info Column Order Tests
|
|
|
|
/// <summary>
|
|
/// Verifies MIS Info columns match expected order per spec.
|
|
/// Legacy order per ExcelWriter.cs lines 299-330:
|
|
/// Item Number, Item Description, MIS Job Step Sequence Number, MIS Number, MIS Revision,
|
|
/// MIS Release Status, MIS Release Date, Branch Code, Job Step Sequence Number,
|
|
/// Matched Sequence Number, Matched to F3112Z1?, Matched to F3003?,
|
|
/// Function Operation Description, Char Number, Test Description,
|
|
/// Sampling Type, Sampling Value, Tools & Gauges, Work Instructions
|
|
///
|
|
/// New implementation reorders to match attribute Order values.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task MisInfo_ColumnOrder_MatchesSpec()
|
|
{
|
|
var search = CreateSearchModelWithMisData();
|
|
var result = await _service.GenerateAsync(search);
|
|
|
|
using var stream = new MemoryStream(result);
|
|
using var workbook = new XLWorkbook(stream);
|
|
var sheet = workbook.Worksheet("MIS Info");
|
|
|
|
var headers = GetHeadersFromSheet(sheet);
|
|
|
|
// Verify column order matches OutputColumn Order attributes
|
|
headers[0].ShouldBe("Item Number");
|
|
headers[1].ShouldBe("MIS Job Step Sequence Number");
|
|
headers[2].ShouldBe("MIS Number");
|
|
headers[3].ShouldBe("MIS Revision");
|
|
headers[4].ShouldBe("Item Description");
|
|
headers[5].ShouldBe("MIS Release Status");
|
|
headers[6].ShouldBe("MIS Release Date");
|
|
headers[7].ShouldBe("Branch Code");
|
|
headers[8].ShouldBe("Job Step Sequence Number");
|
|
headers[9].ShouldBe("Matched Sequence Number");
|
|
headers[10].ShouldBe("Matched to F3112Z1?");
|
|
headers[11].ShouldBe("Matched to F3003?");
|
|
headers[12].ShouldBe("Function Operation Description");
|
|
headers[13].ShouldBe("Char Number");
|
|
headers[14].ShouldBe("Test Description");
|
|
headers[15].ShouldBe("Sampling Type");
|
|
headers[16].ShouldBe("Sampling Value");
|
|
headers[17].ShouldBe("Tools & Gauges");
|
|
headers[18].ShouldBe("Work Instructions");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Investigation Column Order Tests
|
|
|
|
/// <summary>
|
|
/// Verifies Investigation columns match expected order per spec.
|
|
/// Legacy order per ExcelWriter.cs lines 403-418:
|
|
/// Work Center Code, Work Order Number, Work Order Start Date, Job Step Number,
|
|
/// Function Operation Description, Job Step End Date, Function Code,
|
|
/// Item Number, Item Description, Routing Type
|
|
///
|
|
/// New implementation adds: Was Job Step Added?, Matched Job Step Number
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Investigation_ColumnOrder_MatchesSpecWithEnhancements()
|
|
{
|
|
var search = CreateSearchModelWithMisData();
|
|
var result = await _service.GenerateAsync(search);
|
|
|
|
using var stream = new MemoryStream(result);
|
|
using var workbook = new XLWorkbook(stream);
|
|
var sheet = workbook.Worksheet("Investigation");
|
|
|
|
var headers = GetHeadersFromSheet(sheet);
|
|
|
|
headers[0].ShouldBe("Work Center Code");
|
|
headers[1].ShouldBe("Work Order Number");
|
|
headers[2].ShouldBe("Work Order Start Date");
|
|
headers[3].ShouldBe("Job Step Number");
|
|
headers[4].ShouldBe("Function Operation Description");
|
|
headers[5].ShouldBe("Job Step End Date");
|
|
headers[6].ShouldBe("Function Code");
|
|
// New columns per spec
|
|
headers[7].ShouldBe("Was Job Step Added?");
|
|
headers[8].ShouldBe("Matched Job Step Number");
|
|
headers[9].ShouldBe("Item Number");
|
|
headers[10].ShouldBe("Item Description");
|
|
headers[11].ShouldBe("Routing Type");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Format String Tests
|
|
|
|
/// <summary>
|
|
/// Verifies timestamp format matches legacy TIMESTAMP_FORMAT = "[$-409]m/d/yy h:mm AM/PM;@"
|
|
/// per ExcelWriter.cs line 26.
|
|
/// </summary>
|
|
[Fact]
|
|
public void TimestampFormat_MatchesLegacy()
|
|
{
|
|
ExcelFormats.TimestampFormat.ShouldBe("[$-409]m/d/yy h:mm AM/PM;@");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies date format uses locale-aware format.
|
|
/// Legacy used "m/d/yyyy" for Investigation sheet dates.
|
|
/// </summary>
|
|
[Fact]
|
|
public void DateFormat_MatchesLegacyPattern()
|
|
{
|
|
// Legacy used "m/d/yyyy", new implementation uses "[$-409]MM/dd/yyyy;@"
|
|
// Both produce similar output, the new format includes locale specifier
|
|
ExcelFormats.DateFormat.ShouldContain("MM/dd/yyyy");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies wrapped column width matches legacy WRAPPED_CELL_WIDTH = 65
|
|
/// per ExcelWriter.cs line 31.
|
|
/// </summary>
|
|
[Fact]
|
|
public void WrappedColumnWidth_MatchesLegacy()
|
|
{
|
|
ExcelFormats.WrappedColumnWidth.ShouldBe(65);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies criteria sheet padding factor matches legacy 1.15 (15%)
|
|
/// per ExcelWriter.cs line 175.
|
|
/// </summary>
|
|
[Fact]
|
|
public void CriteriaPaddingFactor_MatchesLegacy()
|
|
{
|
|
ExcelFormats.CriteriaPaddingFactor.ShouldBe(1.15);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies data sheet padding factor matches legacy 1.3 (30%)
|
|
/// per ExcelWriter.cs lines 251, 367, 442.
|
|
/// </summary>
|
|
[Fact]
|
|
public void DataPaddingFactor_MatchesLegacy()
|
|
{
|
|
ExcelFormats.DataPaddingFactor.ShouldBe(1.30);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Protection Settings Tests
|
|
|
|
/// <summary>
|
|
/// Verifies criteria sheet uses correct password per ExcelWriter.cs line 179.
|
|
/// Legacy password: "JDE_SCOPING_TOOL_PASS"
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task CriteriaSheet_Protection_IsEnabled()
|
|
{
|
|
var search = CreateSearchModelWithResults();
|
|
var result = await _service.GenerateAsync(search);
|
|
|
|
using var stream = new MemoryStream(result);
|
|
using var workbook = new XLWorkbook(stream);
|
|
var sheet = workbook.Worksheet("Search Criteria");
|
|
|
|
sheet.Protection.IsProtected.ShouldBeTrue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies data sheets use correct password per ExcelWriter.cs line 277, 496.
|
|
/// Legacy password: "JDESCOPINGTOOL"
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task DataSheets_Protection_IsEnabled()
|
|
{
|
|
var search = CreateSearchModelWithMisData();
|
|
var result = await _service.GenerateAsync(search);
|
|
|
|
using var stream = new MemoryStream(result);
|
|
using var workbook = new XLWorkbook(stream);
|
|
|
|
workbook.Worksheet("Search Results").Protection.IsProtected.ShouldBeTrue();
|
|
workbook.Worksheet("MIS Info").Protection.IsProtected.ShouldBeTrue();
|
|
workbook.Worksheet("Investigation").Protection.IsProtected.ShouldBeTrue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies protection allows filtering per legacy settings.
|
|
/// Legacy: AllowAutoFilter = true (line 268)
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task DataSheets_Protection_AllowsFiltering()
|
|
{
|
|
var search = CreateSearchModelWithMisData();
|
|
var result = await _service.GenerateAsync(search);
|
|
|
|
using var stream = new MemoryStream(result);
|
|
using var workbook = new XLWorkbook(stream);
|
|
|
|
var resultsSheet = workbook.Worksheet("Search Results");
|
|
resultsSheet.Protection.AllowedElements.HasFlag(XLSheetProtectionElements.AutoFilter).ShouldBeTrue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies protection allows sorting per legacy settings.
|
|
/// Legacy: AllowSort = true (line 276)
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task DataSheets_Protection_AllowsSorting()
|
|
{
|
|
var search = CreateSearchModelWithMisData();
|
|
var result = await _service.GenerateAsync(search);
|
|
|
|
using var stream = new MemoryStream(result);
|
|
using var workbook = new XLWorkbook(stream);
|
|
|
|
var resultsSheet = workbook.Worksheet("Search Results");
|
|
resultsSheet.Protection.AllowedElements.HasFlag(XLSheetProtectionElements.Sort).ShouldBeTrue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies protection allows formatting per legacy settings.
|
|
/// Legacy: AllowFormatCells, AllowFormatColumns, AllowFormatRows = true (lines 270-272)
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task DataSheets_Protection_AllowsFormatting()
|
|
{
|
|
var search = CreateSearchModelWithMisData();
|
|
var result = await _service.GenerateAsync(search);
|
|
|
|
using var stream = new MemoryStream(result);
|
|
using var workbook = new XLWorkbook(stream);
|
|
|
|
var resultsSheet = workbook.Worksheet("Search Results");
|
|
resultsSheet.Protection.AllowedElements.HasFlag(XLSheetProtectionElements.FormatCells).ShouldBeTrue();
|
|
resultsSheet.Protection.AllowedElements.HasFlag(XLSheetProtectionElements.FormatColumns).ShouldBeTrue();
|
|
resultsSheet.Protection.AllowedElements.HasFlag(XLSheetProtectionElements.FormatRows).ShouldBeTrue();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Table Style Tests
|
|
|
|
/// <summary>
|
|
/// Note: Legacy used TableStyles.Medium1 per ExcelWriter.cs line 261.
|
|
/// New implementation uses Light18 per spec requirement.
|
|
/// This is an intentional change documented in the spec.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task DataSheets_UseLight18TableStyle_PerSpec()
|
|
{
|
|
var search = CreateSearchModelWithMisData();
|
|
var result = await _service.GenerateAsync(search);
|
|
|
|
using var stream = new MemoryStream(result);
|
|
using var workbook = new XLWorkbook(stream);
|
|
|
|
var resultsSheet = workbook.Worksheet("Search Results");
|
|
var table = resultsSheet.Tables.First();
|
|
// Spec specifies Light18, legacy used Medium1
|
|
table.Theme.ShouldBe(XLTableTheme.TableStyleLight18);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Timestamp Formatting Tests
|
|
|
|
/// <summary>
|
|
/// Verifies criteria sheet timestamp format matches legacy.
|
|
/// Legacy format per line 98: "{searchModel.SubmitDT:MMM dd, yyyy hh:mm:ss tt} EST"
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task CriteriaSheet_TimestampFormat_MatchesLegacy()
|
|
{
|
|
var search = CreateSearchModelWithResults();
|
|
search.SubmitDt = new DateTime(2024, 1, 15, 14, 30, 45);
|
|
var result = await _service.GenerateAsync(search);
|
|
|
|
using var stream = new MemoryStream(result);
|
|
using var workbook = new XLWorkbook(stream);
|
|
var sheet = workbook.Worksheet("Search Criteria");
|
|
|
|
var submitTimestamp = sheet.Cell(4, 2).Value.GetText();
|
|
submitTimestamp.ShouldContain("Jan 15, 2024");
|
|
submitTimestamp.ShouldContain("02:30:45");
|
|
submitTimestamp.ShouldContain("EST");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Header Formatting Tests
|
|
|
|
/// <summary>
|
|
/// Verifies header cell formatting matches legacy ApplyHeaderFormat.
|
|
/// Legacy per lines 467-476: Bold, centered, Gainsboro background.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Headers_Formatting_MatchesLegacy()
|
|
{
|
|
var search = CreateSearchModelWithResults();
|
|
var result = await _service.GenerateAsync(search);
|
|
|
|
using var stream = new MemoryStream(result);
|
|
using var workbook = new XLWorkbook(stream);
|
|
var sheet = workbook.Worksheet("Search Criteria");
|
|
|
|
// Check "Search Name" header cell
|
|
var headerCell = sheet.Cell(1, 1);
|
|
headerCell.Style.Font.Bold.ShouldBeTrue();
|
|
headerCell.Style.Fill.BackgroundColor.ShouldBe(XLColor.Gainsboro);
|
|
headerCell.Style.Alignment.Horizontal.ShouldBe(XLAlignmentHorizontalValues.Center);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Helper Methods
|
|
|
|
private static List<string> GetHeadersFromSheet(IXLWorksheet sheet)
|
|
{
|
|
var headers = new List<string>();
|
|
var col = 1;
|
|
while (!sheet.Cell(1, col).IsEmpty())
|
|
{
|
|
headers.Add(sheet.Cell(1, col).Value.GetText());
|
|
col++;
|
|
}
|
|
return headers;
|
|
}
|
|
|
|
private static SearchModel CreateSearchModelWithResults()
|
|
{
|
|
return new SearchModel
|
|
{
|
|
Id = 1,
|
|
Name = "Legacy Comparison Test",
|
|
UserName = "testuser",
|
|
SubmitDt = DateTime.Now.AddHours(-1),
|
|
StartDt = DateTime.Now.AddMinutes(-30),
|
|
EndDt = DateTime.Now,
|
|
ExtractMisData = false,
|
|
Results = [
|
|
new SearchResult
|
|
{
|
|
WorkOrderNumber = 12345,
|
|
WorkOrderBranchCode = "001",
|
|
LotNumber = "LOT-001",
|
|
ItemNumber = "ITEM-001",
|
|
PlanningFamily = "PF01",
|
|
StockingType = "M",
|
|
OrderQuantity = 100,
|
|
HeldQuantity = 0,
|
|
ScrappedQuantity = 0,
|
|
ShippedQuantity = 50,
|
|
StepBranchCode = "001",
|
|
StepNumber = 10,
|
|
StepDescription = "Assembly",
|
|
FunctionOperationDescription = "Main assembly",
|
|
StepUpdateDt = DateTime.Now,
|
|
StatusCode = "50",
|
|
StatusDescription = "In Progress",
|
|
Flagged = true
|
|
}
|
|
]
|
|
};
|
|
}
|
|
|
|
private static SearchModel CreateSearchModelWithMisData()
|
|
{
|
|
var model = CreateSearchModelWithResults();
|
|
model.ExtractMisData = true;
|
|
model.MisResults = [
|
|
new MisSearchResult
|
|
{
|
|
ItemNumber = "ITEM-001",
|
|
SequenceNumber = "010",
|
|
MisNumber = "MIS-001",
|
|
RevId = "A",
|
|
ItemDescription = "Test Item",
|
|
Status = "Released",
|
|
ReleaseDate = DateTime.Now.AddDays(-30),
|
|
BranchCode = "001",
|
|
JobStepSequenceNumber = 10,
|
|
MatchedSequenceNumber = 10,
|
|
RoutingMatch = true,
|
|
MasterMatch = true,
|
|
FunctionOperationDescription = "Assembly operation",
|
|
CharNumber = "001",
|
|
TestDescription = "Sample test description",
|
|
SamplingType = "100%",
|
|
SamplingValue = "1",
|
|
ToolsGauges = "Gauge A, Gauge B",
|
|
WorkInstructions = "Step 1: Do this. Step 2: Do that."
|
|
}
|
|
];
|
|
model.MisNonMatchResults = [
|
|
new MisNonMatchSearchResult
|
|
{
|
|
WorkCenterCode = "WC01",
|
|
WorkOrderNumber = 12345,
|
|
WorkOrderStartDate = DateTime.Now.AddDays(-7),
|
|
JobStepNumber = 10,
|
|
JobStepDescription = "Test operation",
|
|
JobStepEndDate = DateTime.Now.AddDays(-5),
|
|
FunctionCode = "FC01",
|
|
WasJobStepAdded = false,
|
|
MatchedJobStepNumber = 10,
|
|
ItemNumber = "ITEM-001",
|
|
ItemDescription = "Test Item Description",
|
|
RoutingType = "M"
|
|
}
|
|
];
|
|
return model;
|
|
}
|
|
|
|
#endregion
|
|
}
|