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:
Joseph Doherty
2026-01-02 07:43:29 -05:00
commit 26ff8d9b4f
1761 changed files with 596509 additions and 0 deletions
@@ -0,0 +1,187 @@
using ClosedXML.Excel;
using JdeScoping.ExcelIO.Attributes;
using JdeScoping.ExcelIO.Generators;
using JdeScoping.ExcelIO.Helpers;
using Shouldly;
using Xunit;
namespace JdeScoping.ExcelIO.Tests;
public class AttributeTableWriterTests
{
private readonly OutputColumnCache _cache = new();
private readonly AttributeTableWriter _writer;
public AttributeTableWriterTests()
{
_writer = new AttributeTableWriter(_cache);
}
[OutputTable(TabName = "Test Items", TableName = "Test_Items", ShowHeader = false)]
private class TestItem
{
[OutputColumn(Order = 10, HeaderText = "ID")]
public int Id { get; set; }
[OutputColumn(Order = 20, HeaderText = "Name")]
public string Name { get; set; } = string.Empty;
[OutputColumn(Order = 30, HeaderText = "Value")]
public decimal Value { get; set; }
}
[OutputTable(TabName = "Wrapped Table", TableName = "Wrapped_Table")]
private class WrappedItem
{
[OutputColumn(Order = 10, HeaderText = "Description", WrapText = true, AutoWidth = false, Width = 65)]
public string Description { get; set; } = string.Empty;
}
private class NoAttributeItem
{
public string Data { get; set; } = string.Empty;
}
[Fact]
public void WriteTable_CreatesTableWithCorrectColumns()
{
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Test");
var data = new List<TestItem>
{
new() { Id = 1, Name = "Item 1", Value = 10.5m },
new() { Id = 2, Name = "Item 2", Value = 20.5m }
};
var table = _writer.WriteTable(worksheet, 1, 1, data);
table.ShouldNotBeNull();
table.ColumnCount().ShouldBe(3);
table.RowCount().ShouldBe(3); // Header + 2 data rows
}
[Fact]
public void WriteTable_UsesLight18TableStyle()
{
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Test");
var data = new List<TestItem> { new() { Id = 1, Name = "Test", Value = 100m } };
var table = _writer.WriteTable(worksheet, 1, 1, data);
table.ShouldNotBeNull();
table.Theme.ShouldBe(XLTableTheme.TableStyleLight18);
}
[Fact]
public void WriteTable_SetsColumnHeaders()
{
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Test");
var data = new List<TestItem> { new() { Id = 1, Name = "Test", Value = 100m } };
_writer.WriteTable(worksheet, 1, 1, data);
worksheet.Cell(1, 1).Value.GetText().ShouldBe("ID");
worksheet.Cell(1, 2).Value.GetText().ShouldBe("Name");
worksheet.Cell(1, 3).Value.GetText().ShouldBe("Value");
}
[Fact]
public void WriteTable_WritesDataRows()
{
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Test");
var data = new List<TestItem>
{
new() { Id = 1, Name = "Item 1", Value = 10.5m },
new() { Id = 2, Name = "Item 2", Value = 20.5m }
};
_writer.WriteTable(worksheet, 1, 1, data);
worksheet.Cell(2, 1).Value.GetNumber().ShouldBe(1);
worksheet.Cell(2, 2).Value.GetText().ShouldBe("Item 1");
worksheet.Cell(2, 3).Value.GetNumber().ShouldBe(10.5);
worksheet.Cell(3, 1).Value.GetNumber().ShouldBe(2);
}
[Fact]
public void WriteTable_WithShowHeader_CreatesMergedHeader()
{
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Test");
var data = new List<TestItem> { new() { Id = 1, Name = "Test", Value = 100m } };
_writer.WriteTable(worksheet, 1, 1, data, showHeader: true, headerText: "Test Header");
// First row should be merged header
var headerRange = worksheet.Range(1, 1, 1, 3);
headerRange.IsMerged().ShouldBeTrue();
worksheet.Cell(1, 1).Value.GetText().ShouldBe("Test Header");
// Column headers should be on row 2
worksheet.Cell(2, 1).Value.GetText().ShouldBe("ID");
}
[Fact]
public void WriteTable_EmptyData_CreatesTableWithHeaderOnly()
{
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Test");
var data = new List<TestItem>();
var table = _writer.WriteTable(worksheet, 1, 1, data);
table.ShouldNotBeNull();
// Table should exist with headers
table.ColumnCount().ShouldBe(3);
}
[Fact]
public void WriteTable_NoAttributes_ReturnsNull()
{
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Test");
var data = new List<NoAttributeItem> { new() { Data = "Test" } };
var table = _writer.WriteTable(worksheet, 1, 1, data);
table.ShouldBeNull();
}
[Fact]
public void WriteTable_WrappedColumn_SetsFixedWidth()
{
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Test");
var data = new List<WrappedItem> { new() { Description = "Long description text" } };
_writer.WriteTable(worksheet, 1, 1, data);
worksheet.Column(1).Width.ShouldBe(65);
worksheet.Column(1).Style.Alignment.WrapText.ShouldBeTrue();
}
[Fact]
public void WriteTable_TableNameOverride_UsesProvidedName()
{
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Test");
var data = new List<TestItem> { new() { Id = 1, Name = "Test", Value = 100m } };
var table = _writer.WriteTable(worksheet, 1, 1, data, tableNameOverride: "Custom_Table");
table.ShouldNotBeNull();
table.Name.ShouldBe("Custom_Table");
}
}
@@ -0,0 +1,116 @@
using ClosedXML.Excel;
using JdeScoping.ExcelIO.Attributes;
using JdeScoping.ExcelIO.Formatting;
using Shouldly;
using Xunit;
namespace JdeScoping.ExcelIO.Tests;
public class ColumnFormatterTests
{
[Fact]
public void ApplyColumnFormat_AutoWidth_AdjustsToContents()
{
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Test");
worksheet.Cell(1, 1).Value = "Some Text Value";
var attr = new OutputColumnAttribute
{
AutoWidth = true,
Format = OutputColumnAttribute.StdFormat
};
ColumnFormatter.ApplyColumnFormat(worksheet.Column(1), attr);
// Width should be greater than default after adjustment
worksheet.Column(1).Width.ShouldBeGreaterThan(0);
}
[Fact]
public void ApplyColumnFormat_FixedWidth_SetsExactWidth()
{
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Test");
var attr = new OutputColumnAttribute
{
AutoWidth = false,
Width = 50.0,
Format = OutputColumnAttribute.StdFormat
};
ColumnFormatter.ApplyColumnFormat(worksheet.Column(1), attr);
worksheet.Column(1).Width.ShouldBe(50.0);
}
[Fact]
public void ApplyColumnFormat_WrapText_EnablesWrapping()
{
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Test");
var attr = new OutputColumnAttribute
{
WrapText = true,
AutoWidth = false,
Width = 65.0,
Format = OutputColumnAttribute.StdFormat
};
ColumnFormatter.ApplyColumnFormat(worksheet.Column(1), attr);
worksheet.Column(1).Style.Alignment.WrapText.ShouldBeTrue();
worksheet.Column(1).Width.ShouldBe(65.0);
}
[Fact]
public void ApplyColumnFormat_DateFormat_AppliesCorrectFormat()
{
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Test");
var attr = new OutputColumnAttribute
{
AutoWidth = false,
Width = 20.0,
Format = OutputColumnAttribute.DateFormat
};
ColumnFormatter.ApplyColumnFormat(worksheet.Column(1), attr);
worksheet.Column(1).Style.NumberFormat.Format.ShouldBe(OutputColumnAttribute.DateFormat);
}
[Fact]
public void ApplyColumnFormat_TimestampFormat_AppliesCorrectFormat()
{
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Test");
var attr = new OutputColumnAttribute
{
AutoWidth = false,
Width = 25.0,
Format = OutputColumnAttribute.TimestampFormat
};
ColumnFormatter.ApplyColumnFormat(worksheet.Column(1), attr);
worksheet.Column(1).Style.NumberFormat.Format.ShouldBe(OutputColumnAttribute.TimestampFormat);
}
[Fact]
public void AutoFitWithPadding_AppliesPaddingFactor()
{
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Test");
worksheet.Cell(1, 1).Value = "Some Text";
ColumnFormatter.AutoFitWithPadding(worksheet.Column(1), 1.30);
// Width should be greater than 0 and include padding
worksheet.Column(1).Width.ShouldBeGreaterThan(0);
}
}
@@ -0,0 +1,416 @@
using ClosedXML.Excel;
using JdeScoping.ExcelIO.Configuration;
using JdeScoping.ExcelIO.Generators;
using JdeScoping.ExcelIO.Helpers;
using JdeScoping.ExcelIO.Models.Reporting;
using Microsoft.Extensions.Options;
using Shouldly;
using Xunit;
namespace JdeScoping.ExcelIO.Tests;
public class CriteriaSheetGeneratorTests
{
private readonly CriteriaSheetGenerator _generator;
private readonly IOptions<ExcelExportOptions> _options;
public CriteriaSheetGeneratorTests()
{
_options = Options.Create(new ExcelExportOptions
{
CriteriaSheetPassword = "TestPassword"
});
var cache = new OutputColumnCache();
var tableWriter = new AttributeTableWriter(cache);
_generator = new CriteriaSheetGenerator(_options, tableWriter);
}
[Fact]
public void Generate_CreatesSearchCriteriaSheet()
{
using var workbook = new XLWorkbook();
var search = CreateMinimalSearchModel();
_generator.Generate(workbook, search);
workbook.Worksheets.TryGetWorksheet("Search Criteria", out var worksheet).ShouldBeTrue();
worksheet.ShouldNotBeNull();
}
[Fact]
public void Generate_ContainsSearchName()
{
using var workbook = new XLWorkbook();
var search = CreateMinimalSearchModel();
search.Name = "Test Search Name";
_generator.Generate(workbook, search);
var worksheet = workbook.Worksheet("Search Criteria");
worksheet.Cell(1, 1).Value.GetText().ShouldBe("Search Name");
worksheet.Cell(1, 2).Value.GetText().ShouldBe("Test Search Name");
}
[Fact]
public void Generate_ContainsUserName()
{
using var workbook = new XLWorkbook();
var search = CreateMinimalSearchModel();
search.UserName = "testuser";
_generator.Generate(workbook, search);
var worksheet = workbook.Worksheet("Search Criteria");
worksheet.Cell(2, 1).Value.GetText().ShouldBe("User Name");
worksheet.Cell(2, 2).Value.GetText().ShouldBe("testuser");
}
[Fact]
public void Generate_ContainsTimestamps()
{
using var workbook = new XLWorkbook();
var search = CreateMinimalSearchModel();
var submitDt = new DateTime(2024, 1, 15, 10, 30, 0);
var startDt = new DateTime(2024, 1, 15, 10, 31, 0);
var endDt = new DateTime(2024, 1, 15, 10, 35, 0);
search.SubmitDt = submitDt;
search.StartDt = startDt;
search.EndDt = endDt;
_generator.Generate(workbook, search);
var worksheet = workbook.Worksheet("Search Criteria");
worksheet.Cell(4, 1).Value.GetText().ShouldBe("Submit timestamp");
worksheet.Cell(4, 2).Value.GetText().ShouldContain("Jan 15, 2024");
worksheet.Cell(5, 1).Value.GetText().ShouldBe("Start timestamp");
worksheet.Cell(5, 2).Value.GetText().ShouldContain("Jan 15, 2024");
worksheet.Cell(6, 1).Value.GetText().ShouldBe("Completed timestamp");
worksheet.Cell(6, 2).Value.GetText().ShouldContain("Jan 15, 2024");
}
[Fact]
public void Generate_ContainsTimespanFilterTable()
{
using var workbook = new XLWorkbook();
var search = CreateMinimalSearchModel();
search.MinimumDt = new DateTime(2024, 1, 1);
search.MaximumDt = new DateTime(2024, 12, 31);
_generator.Generate(workbook, search);
var worksheet = workbook.Worksheet("Search Criteria");
var tables = worksheet.Tables;
tables.Any(t => t.Name == "Timespan_Filter").ShouldBeTrue();
}
[Fact]
public void Generate_ContainsWorkOrderFilterTable()
{
using var workbook = new XLWorkbook();
var search = CreateMinimalSearchModel();
search.WorkOrderFilter.Add(new WorkOrderFilterEntry { WorkOrderNumber = 12345, ItemNumber = "ITEM-001" });
_generator.Generate(workbook, search);
var worksheet = workbook.Worksheet("Search Criteria");
var tables = worksheet.Tables;
tables.Any(t => t.Name == "Work_Order_Filter").ShouldBeTrue();
}
[Fact]
public void Generate_ContainsItemNumberFilterTable()
{
using var workbook = new XLWorkbook();
var search = CreateMinimalSearchModel();
search.ItemNumberFilter.Add(new ItemNumberFilterEntry { ItemNumber = "ITEM-001" });
_generator.Generate(workbook, search);
var worksheet = workbook.Worksheet("Search Criteria");
var tables = worksheet.Tables;
tables.Any(t => t.Name == "Item_Number_Filter").ShouldBeTrue();
}
[Fact]
public void Generate_ContainsProfitCenterFilterTable()
{
using var workbook = new XLWorkbook();
var search = CreateMinimalSearchModel();
search.ProfitCenterFilter.Add(new ProfitCenterFilterEntry { Code = "PC01", Description = "Profit Center 1" });
_generator.Generate(workbook, search);
var worksheet = workbook.Worksheet("Search Criteria");
var tables = worksheet.Tables;
tables.Any(t => t.Name == "Profit_Center_Filter").ShouldBeTrue();
}
[Fact]
public void Generate_ContainsWorkCenterFilterTable()
{
using var workbook = new XLWorkbook();
var search = CreateMinimalSearchModel();
search.WorkCenterFilter.Add(new WorkCenterFilterEntry { Code = "WC01", Description = "Work Center 1" });
_generator.Generate(workbook, search);
var worksheet = workbook.Worksheet("Search Criteria");
var tables = worksheet.Tables;
tables.Any(t => t.Name == "Work_Center_Filter").ShouldBeTrue();
}
[Fact]
public void Generate_ContainsOperatorFilterTable()
{
using var workbook = new XLWorkbook();
var search = CreateMinimalSearchModel();
search.OperatorFilter.Add(new OperatorFilterEntry { UserId = "OP01", FullName = "Operator 1" });
_generator.Generate(workbook, search);
var worksheet = workbook.Worksheet("Search Criteria");
var tables = worksheet.Tables;
tables.Any(t => t.Name == "Operator_Filter").ShouldBeTrue();
}
[Fact]
public void Generate_ContainsComponentLotFilterTable()
{
using var workbook = new XLWorkbook();
var search = CreateMinimalSearchModel();
search.ComponentLotFilter.Add(new ComponentLotFilterEntry { LotNumber = "LOT001" });
_generator.Generate(workbook, search);
var worksheet = workbook.Worksheet("Search Criteria");
var tables = worksheet.Tables;
tables.Any(t => t.Name == "Component_Lot_Filter").ShouldBeTrue();
}
[Fact]
public void Generate_ContainsItemOperationMisFilterTable()
{
using var workbook = new XLWorkbook();
var search = CreateMinimalSearchModel();
search.ItemOperationMisFilter.Add(new ItemOperationMisFilterEntry
{
ItemNumber = "ITEM-001",
OperationNumber = "10",
MisNumber = "MIS-001"
});
_generator.Generate(workbook, search);
var worksheet = workbook.Worksheet("Search Criteria");
var tables = worksheet.Tables;
tables.Any(t => t.Name == "Item_Operation_MIS_Filter").ShouldBeTrue();
}
[Fact]
public void Generate_ContainsExtractMisDataIndicator_WhenTrue()
{
using var workbook = new XLWorkbook();
var search = CreateMinimalSearchModel();
search.ExtractMisData = true;
_generator.Generate(workbook, search);
var worksheet = workbook.Worksheet("Search Criteria");
// Find the "Extract MIS data?" header and check for YES
var cells = worksheet.CellsUsed();
var extractMisCell = cells.FirstOrDefault(c => c.Value.ToString() == "YES" || c.Value.ToString() == "NO");
extractMisCell.ShouldNotBeNull();
extractMisCell.Value.GetText().ShouldBe("YES");
}
[Fact]
public void Generate_ContainsExtractMisDataIndicator_WhenFalse()
{
using var workbook = new XLWorkbook();
var search = CreateMinimalSearchModel();
search.ExtractMisData = false;
_generator.Generate(workbook, search);
var worksheet = workbook.Worksheet("Search Criteria");
// Find the "Extract MIS data?" header and check for NO
var cells = worksheet.CellsUsed();
var extractMisCell = cells.FirstOrDefault(c => c.Value.ToString() == "YES" || c.Value.ToString() == "NO");
extractMisCell.ShouldNotBeNull();
extractMisCell.Value.GetText().ShouldBe("NO");
}
[Fact]
public void Generate_AppliesHeaderFormatting()
{
using var workbook = new XLWorkbook();
var search = CreateMinimalSearchModel();
_generator.Generate(workbook, search);
var worksheet = workbook.Worksheet("Search Criteria");
// Search Name header should be bold with Gainsboro background
var headerCell = worksheet.Cell(1, 1);
headerCell.Style.Font.Bold.ShouldBeTrue();
headerCell.Style.Fill.BackgroundColor.ShouldBe(XLColor.Gainsboro);
}
[Fact]
public void Generate_AppliesProtection()
{
using var workbook = new XLWorkbook();
var search = CreateMinimalSearchModel();
_generator.Generate(workbook, search);
var worksheet = workbook.Worksheet("Search Criteria");
worksheet.Protection.IsProtected.ShouldBeTrue();
}
[Fact]
public void Generate_TablesHaveLight18Style()
{
using var workbook = new XLWorkbook();
var search = CreateMinimalSearchModel();
search.WorkOrderFilter.Add(new WorkOrderFilterEntry { WorkOrderNumber = 12345, ItemNumber = "ITEM-001" });
_generator.Generate(workbook, search);
var worksheet = workbook.Worksheet("Search Criteria");
var table = worksheet.Tables.First(t => t.Name == "Work_Order_Filter");
table.Theme.ShouldBe(XLTableTheme.TableStyleLight18);
}
[Fact]
public void Generate_FilterTables_Have2BlankRowSpacing()
{
using var workbook = new XLWorkbook();
var search = CreateMinimalSearchModel();
search.WorkOrderFilter.Add(new WorkOrderFilterEntry { WorkOrderNumber = 12345, ItemNumber = "ITEM-001" });
search.ItemNumberFilter.Add(new ItemNumberFilterEntry { ItemNumber = "ITEM-001" });
_generator.Generate(workbook, search);
var worksheet = workbook.Worksheet("Search Criteria");
var woTable = worksheet.Tables.First(t => t.Name == "Work_Order_Filter");
var itemTable = worksheet.Tables.First(t => t.Name == "Item_Number_Filter");
// There should be 2 blank rows between tables. With the header row of the next table, that's a gap of 3
// Looking at CriteriaSheetGenerator: row = table.RangeAddress.LastAddress.RowNumber + 3
// This means the next table starts 3 rows after the last row, leaving 2 blank rows in between
var gap = itemTable.RangeAddress.FirstAddress.RowNumber - woTable.RangeAddress.LastAddress.RowNumber;
// Gap includes header row of next table, so: 2 blank rows + 1 header = gap of 3
// But with table header (Timespan_Filter has ShowHeader=true), add 1 more
gap.ShouldBeGreaterThanOrEqualTo(3);
}
[Fact]
public void Generate_NullTimestamps_ShowEmptyValue()
{
using var workbook = new XLWorkbook();
var search = CreateMinimalSearchModel();
search.SubmitDt = null;
search.StartDt = null;
search.EndDt = null;
_generator.Generate(workbook, search);
var worksheet = workbook.Worksheet("Search Criteria");
worksheet.Cell(4, 2).Value.ToString().ShouldBe(string.Empty);
worksheet.Cell(5, 2).Value.ToString().ShouldBe(string.Empty);
worksheet.Cell(6, 2).Value.ToString().ShouldBe(string.Empty);
}
[Fact]
public void Generate_ColumnsAreAutoFitWithPadding()
{
using var workbook = new XLWorkbook();
var search = CreateMinimalSearchModel();
search.Name = "A Very Long Search Name That Needs Extra Width";
_generator.Generate(workbook, search);
var worksheet = workbook.Worksheet("Search Criteria");
// Columns should have been adjusted - verify they have non-default width
worksheet.Column(1).Width.ShouldBeGreaterThan(0);
worksheet.Column(2).Width.ShouldBeGreaterThan(0);
}
[Fact]
public void Generate_MultipleFiltersWithData_CreatesAllTables()
{
using var workbook = new XLWorkbook();
var search = CreateFullSearchModel();
_generator.Generate(workbook, search);
var worksheet = workbook.Worksheet("Search Criteria");
var tables = worksheet.Tables;
// Should have 8 filter tables
tables.Count().ShouldBe(8);
tables.Any(t => t.Name == "Timespan_Filter").ShouldBeTrue();
tables.Any(t => t.Name == "Work_Order_Filter").ShouldBeTrue();
tables.Any(t => t.Name == "Item_Number_Filter").ShouldBeTrue();
tables.Any(t => t.Name == "Profit_Center_Filter").ShouldBeTrue();
tables.Any(t => t.Name == "Work_Center_Filter").ShouldBeTrue();
tables.Any(t => t.Name == "Component_Lot_Filter").ShouldBeTrue();
tables.Any(t => t.Name == "Operator_Filter").ShouldBeTrue();
tables.Any(t => t.Name == "Item_Operation_MIS_Filter").ShouldBeTrue();
}
[Fact]
public void Generate_TimestampFormat_IncludesESTSuffix()
{
using var workbook = new XLWorkbook();
var search = CreateMinimalSearchModel();
search.SubmitDt = new DateTime(2024, 1, 15, 10, 30, 45);
_generator.Generate(workbook, search);
var worksheet = workbook.Worksheet("Search Criteria");
var timestampValue = worksheet.Cell(4, 2).Value.GetText();
timestampValue.ShouldContain("EST");
timestampValue.ShouldContain("10:30:45");
}
private static SearchModel CreateMinimalSearchModel()
{
return new SearchModel
{
Id = 1,
Name = "Test Search",
UserName = "testuser",
SubmitDt = DateTime.Now.AddHours(-1),
StartDt = DateTime.Now.AddMinutes(-30),
EndDt = DateTime.Now,
ExtractMisData = false
};
}
private static SearchModel CreateFullSearchModel()
{
return new SearchModel
{
Id = 1,
Name = "Full Search",
UserName = "testuser",
SubmitDt = DateTime.Now.AddHours(-1),
StartDt = DateTime.Now.AddMinutes(-30),
EndDt = DateTime.Now,
MinimumDt = new DateTime(2024, 1, 1),
MaximumDt = new DateTime(2024, 12, 31),
ExtractMisData = true,
WorkOrderFilter = [new WorkOrderFilterEntry { WorkOrderNumber = 12345, ItemNumber = "ITEM-001" }],
ItemNumberFilter = [new ItemNumberFilterEntry { ItemNumber = "ITEM-001" }],
ProfitCenterFilter = [new ProfitCenterFilterEntry { Code = "PC01", Description = "Profit Center 1" }],
WorkCenterFilter = [new WorkCenterFilterEntry { Code = "WC01", Description = "Work Center 1" }],
OperatorFilter = [new OperatorFilterEntry { UserId = "OP01", FullName = "Operator 1" }],
ComponentLotFilter = [new ComponentLotFilterEntry { LotNumber = "LOT001" }],
ItemOperationMisFilter = [new ItemOperationMisFilterEntry { ItemNumber = "ITEM-001", OperationNumber = "10", MisNumber = "MIS-001" }]
};
}
}
@@ -0,0 +1,145 @@
using ClosedXML.Excel;
using JdeScoping.ExcelIO.Generators;
using Shouldly;
using Xunit;
namespace JdeScoping.ExcelIO.Tests;
public class DataEntryTemplateGeneratorTests
{
private readonly DataEntryTemplateGenerator _generator = new();
[Fact]
public void Generate_SingleColumn_ReturnsValidExcel()
{
var result = _generator.Generate<string>(null, "Test Header");
result.ShouldNotBeNull();
result.Length.ShouldBeGreaterThan(0);
using var stream = new MemoryStream(result);
using var workbook = new XLWorkbook(stream);
workbook.Worksheets.Count.ShouldBe(1);
}
[Fact]
public void Generate_SingleColumn_HasCorrectHeader()
{
var result = _generator.Generate<string>(null, "Item Numbers");
using var stream = new MemoryStream(result);
using var workbook = new XLWorkbook(stream);
var worksheet = workbook.Worksheets.First();
worksheet.Cell(1, 1).Value.GetText().ShouldBe("Item Numbers");
}
[Fact]
public void Generate_SingleColumn_HasHeaderFormatting()
{
var result = _generator.Generate<string>(null, "Test Header");
using var stream = new MemoryStream(result);
using var workbook = new XLWorkbook(stream);
var worksheet = workbook.Worksheets.First();
var headerCell = worksheet.Cell(1, 1);
headerCell.Style.Font.Bold.ShouldBeTrue();
headerCell.Style.Fill.BackgroundColor.ShouldBe(XLColor.Gainsboro);
}
[Fact]
public void Generate_SingleColumn_WithData_PopulatesRows()
{
var data = new List<string> { "Item1", "Item2", "Item3" };
var result = _generator.Generate(data, "Items");
using var stream = new MemoryStream(result);
using var workbook = new XLWorkbook(stream);
var worksheet = workbook.Worksheets.First();
worksheet.Cell(2, 1).Value.GetText().ShouldBe("Item1");
worksheet.Cell(3, 1).Value.GetText().ShouldBe("Item2");
worksheet.Cell(4, 1).Value.GetText().ShouldBe("Item3");
}
[Fact]
public void Generate_SingleColumn_SetsTextFormat()
{
var result = _generator.Generate<string>(null, "Test");
using var stream = new MemoryStream(result);
using var workbook = new XLWorkbook(stream);
var worksheet = workbook.Worksheets.First();
worksheet.Column(1).Style.NumberFormat.Format.ShouldBe("@");
}
[Fact]
public void Generate_MultiColumn_ReturnsValidExcel()
{
var headers = new[] { "Column A", "Column B", "Column C" };
var result = _generator.Generate(null, headers);
result.ShouldNotBeNull();
using var stream = new MemoryStream(result);
using var workbook = new XLWorkbook(stream);
var worksheet = workbook.Worksheets.First();
worksheet.Cell(1, 1).Value.GetText().ShouldBe("Column A");
worksheet.Cell(1, 2).Value.GetText().ShouldBe("Column B");
worksheet.Cell(1, 3).Value.GetText().ShouldBe("Column C");
}
[Fact]
public void Generate_MultiColumn_WithData_PopulatesRows()
{
var headers = new[] { "Name", "Value" };
var data = new[]
{
new object[] { "Row1", 100 },
new object[] { "Row2", 200 }
};
var result = _generator.Generate(data, headers);
using var stream = new MemoryStream(result);
using var workbook = new XLWorkbook(stream);
var worksheet = workbook.Worksheets.First();
worksheet.Cell(2, 1).Value.GetText().ShouldBe("Row1");
worksheet.Cell(2, 2).Value.GetNumber().ShouldBe(100);
worksheet.Cell(3, 1).Value.GetText().ShouldBe("Row2");
worksheet.Cell(3, 2).Value.GetNumber().ShouldBe(200);
}
[Fact]
public void Generate_MultiColumn_SetsColumnWidth()
{
var headers = new[] { "Column A", "Column B" };
var result = _generator.Generate(null, headers);
using var stream = new MemoryStream(result);
using var workbook = new XLWorkbook(stream);
var worksheet = workbook.Worksheets.First();
worksheet.Column(1).Width.ShouldBe(65);
worksheet.Column(2).Width.ShouldBe(65);
}
[Fact]
public void Generate_SingleColumn_SetsColumnWidth()
{
var result = _generator.Generate<string>(null, "Test");
using var stream = new MemoryStream(result);
using var workbook = new XLWorkbook(stream);
var worksheet = workbook.Worksheets.First();
worksheet.Column(1).Width.ShouldBe(45);
}
}
@@ -0,0 +1,638 @@
using ClosedXML.Excel;
using JdeScoping.ExcelIO.Configuration;
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>
/// Integration tests that generate actual .xlsx files and verify structure with ClosedXML.
/// </summary>
public class ExcelExportIntegrationTests
{
private readonly ExcelExportService _service;
private readonly ILogger<ExcelExportService> _logger;
private readonly IOptions<ExcelExportOptions> _options;
public ExcelExportIntegrationTests()
{
_logger = Substitute.For<ILogger<ExcelExportService>>();
_options = Options.Create(new ExcelExportOptions
{
CriteriaSheetPassword = "TestCriteriaPass",
DataSheetPassword = "TestDataPass"
});
var cache = new OutputColumnCache();
var tableWriter = new AttributeTableWriter(cache);
var criteriaGenerator = new CriteriaSheetGenerator(_options, tableWriter);
_service = new ExcelExportService(_logger, _options, criteriaGenerator, tableWriter);
}
#region Sheet Count Tests
[Fact]
public async Task GenerateAsync_MinimalSearch_HasTwoSheets()
{
var search = CreateMinimalSearchModel();
var result = await _service.GenerateAsync(search);
using var stream = new MemoryStream(result);
using var workbook = new XLWorkbook(stream);
workbook.Worksheets.Count.ShouldBe(2);
}
[Fact]
public async Task GenerateAsync_WithMisData_HasFourSheets()
{
var search = CreateSearchModelWithMisData();
var result = await _service.GenerateAsync(search);
using var stream = new MemoryStream(result);
using var workbook = new XLWorkbook(stream);
workbook.Worksheets.Count.ShouldBe(4);
}
#endregion
#region Sheet Name Tests
[Fact]
public async Task GenerateAsync_CreatesSearchCriteriaSheet()
{
var search = CreateMinimalSearchModel();
var result = await _service.GenerateAsync(search);
using var stream = new MemoryStream(result);
using var workbook = new XLWorkbook(stream);
workbook.Worksheets.TryGetWorksheet("Search Criteria", out _).ShouldBeTrue();
}
[Fact]
public async Task GenerateAsync_CreatesSearchResultsSheet()
{
var search = CreateMinimalSearchModel();
var result = await _service.GenerateAsync(search);
using var stream = new MemoryStream(result);
using var workbook = new XLWorkbook(stream);
workbook.Worksheets.TryGetWorksheet("Search Results", out _).ShouldBeTrue();
}
[Fact]
public async Task GenerateAsync_WithMisData_CreatesMisInfoSheet()
{
var search = CreateSearchModelWithMisData();
var result = await _service.GenerateAsync(search);
using var stream = new MemoryStream(result);
using var workbook = new XLWorkbook(stream);
workbook.Worksheets.TryGetWorksheet("MIS Info", out _).ShouldBeTrue();
}
[Fact]
public async Task GenerateAsync_WithMisData_CreatesInvestigationSheet()
{
var search = CreateSearchModelWithMisData();
var result = await _service.GenerateAsync(search);
using var stream = new MemoryStream(result);
using var workbook = new XLWorkbook(stream);
workbook.Worksheets.TryGetWorksheet("Investigation", out _).ShouldBeTrue();
}
#endregion
#region Column Header Tests - Search Results
[Fact]
public async Task GenerateAsync_SearchResultsSheet_Has19ColumnHeaders()
{
var search = CreateMinimalSearchModel();
search.Results.Add(CreateSampleSearchResult());
var result = await _service.GenerateAsync(search);
using var stream = new MemoryStream(result);
using var workbook = new XLWorkbook(stream);
var resultsSheet = workbook.Worksheet("Search Results");
// Count non-empty cells in first row to get column count
var headers = GetHeadersFromSheet(resultsSheet);
headers.Count.ShouldBe(19);
}
[Fact]
public async Task GenerateAsync_SearchResultsSheet_HasCorrectColumnHeaders()
{
var search = CreateMinimalSearchModel();
search.Results.Add(CreateSampleSearchResult());
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 headers = GetHeadersFromSheet(resultsSheet);
headers.ShouldContain("Work Order Number");
headers.ShouldContain("Work Order Branch Code");
headers.ShouldContain("Lot Number");
headers.ShouldContain("Item Number");
headers.ShouldContain("Planning Family");
headers.ShouldContain("Stocking Type");
headers.ShouldContain("Order Quantity");
headers.ShouldContain("Held Quantity");
headers.ShouldContain("Scrapped Quantity");
headers.ShouldContain("Shipped Quantity");
headers.ShouldContain("Operation Step Branch Code");
headers.ShouldContain("Operation Step");
headers.ShouldContain("Operation Step Description");
headers.ShouldContain("Function Operation Description");
headers.ShouldContain("Operation Step Update Timestamp");
headers.ShouldContain("Status Code");
headers.ShouldContain("Status Description");
headers.ShouldContain("Status Update Timestamp");
headers.ShouldContain("Inclusion Reason");
}
#endregion
#region Column Header Tests - MIS Info
[Fact]
public async Task GenerateAsync_MisInfoSheet_Has19ColumnHeaders()
{
var search = CreateSearchModelWithMisData();
var result = await _service.GenerateAsync(search);
using var stream = new MemoryStream(result);
using var workbook = new XLWorkbook(stream);
var misSheet = workbook.Worksheet("MIS Info");
var headers = GetHeadersFromSheet(misSheet);
headers.Count.ShouldBe(19);
}
[Fact]
public async Task GenerateAsync_MisInfoSheet_HasCorrectColumnHeaders()
{
var search = CreateSearchModelWithMisData();
var result = await _service.GenerateAsync(search);
using var stream = new MemoryStream(result);
using var workbook = new XLWorkbook(stream);
var misSheet = workbook.Worksheet("MIS Info");
var headers = GetHeadersFromSheet(misSheet);
headers.ShouldContain("Item Number");
headers.ShouldContain("MIS Job Step Sequence Number");
headers.ShouldContain("MIS Number");
headers.ShouldContain("MIS Revision");
headers.ShouldContain("Item Description");
headers.ShouldContain("MIS Release Status");
headers.ShouldContain("MIS Release Date");
headers.ShouldContain("Branch Code");
headers.ShouldContain("Job Step Sequence Number");
headers.ShouldContain("Matched Sequence Number");
headers.ShouldContain("Matched to F3112Z1?");
headers.ShouldContain("Matched to F3003?");
headers.ShouldContain("Function Operation Description");
headers.ShouldContain("Char Number");
headers.ShouldContain("Test Description");
headers.ShouldContain("Sampling Type");
headers.ShouldContain("Sampling Value");
headers.ShouldContain("Tools & Gauges");
headers.ShouldContain("Work Instructions");
}
#endregion
#region Column Header Tests - Investigation
[Fact]
public async Task GenerateAsync_InvestigationSheet_Has12ColumnHeaders()
{
var search = CreateSearchModelWithMisData();
var result = await _service.GenerateAsync(search);
using var stream = new MemoryStream(result);
using var workbook = new XLWorkbook(stream);
var investigationSheet = workbook.Worksheet("Investigation");
var headers = GetHeadersFromSheet(investigationSheet);
headers.Count.ShouldBe(12);
}
[Fact]
public async Task GenerateAsync_InvestigationSheet_HasCorrectColumnHeaders()
{
var search = CreateSearchModelWithMisData();
var result = await _service.GenerateAsync(search);
using var stream = new MemoryStream(result);
using var workbook = new XLWorkbook(stream);
var investigationSheet = workbook.Worksheet("Investigation");
var headers = GetHeadersFromSheet(investigationSheet);
headers.ShouldContain("Work Center Code");
headers.ShouldContain("Work Order Number");
headers.ShouldContain("Work Order Start Date");
headers.ShouldContain("Job Step Number");
headers.ShouldContain("Function Operation Description");
headers.ShouldContain("Job Step End Date");
headers.ShouldContain("Function Code");
headers.ShouldContain("Was Job Step Added?");
headers.ShouldContain("Matched Job Step Number");
headers.ShouldContain("Item Number");
headers.ShouldContain("Item Description");
headers.ShouldContain("Routing Type");
}
#endregion
#region Table Style Tests
[Fact]
public async Task GenerateAsync_SearchResultsTable_UsesLight18Style()
{
var search = CreateMinimalSearchModel();
search.Results.Add(CreateSampleSearchResult());
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();
table.Theme.ShouldBe(XLTableTheme.TableStyleLight18);
}
[Fact]
public async Task GenerateAsync_MisInfoTable_UsesLight18Style()
{
var search = CreateSearchModelWithMisData();
var result = await _service.GenerateAsync(search);
using var stream = new MemoryStream(result);
using var workbook = new XLWorkbook(stream);
var misSheet = workbook.Worksheet("MIS Info");
var table = misSheet.Tables.First();
table.Theme.ShouldBe(XLTableTheme.TableStyleLight18);
}
[Fact]
public async Task GenerateAsync_InvestigationTable_UsesLight18Style()
{
var search = CreateSearchModelWithMisData();
var result = await _service.GenerateAsync(search);
using var stream = new MemoryStream(result);
using var workbook = new XLWorkbook(stream);
var investigationSheet = workbook.Worksheet("Investigation");
var table = investigationSheet.Tables.First();
table.Theme.ShouldBe(XLTableTheme.TableStyleLight18);
}
#endregion
#region Protection Tests
[Fact]
public async Task GenerateAsync_SearchCriteriaSheet_IsProtected()
{
var search = CreateMinimalSearchModel();
var result = await _service.GenerateAsync(search);
using var stream = new MemoryStream(result);
using var workbook = new XLWorkbook(stream);
var criteriaSheet = workbook.Worksheet("Search Criteria");
criteriaSheet.Protection.IsProtected.ShouldBeTrue();
}
[Fact]
public async Task GenerateAsync_SearchResultsSheet_IsProtected()
{
var search = CreateMinimalSearchModel();
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.IsProtected.ShouldBeTrue();
}
[Fact]
public async Task GenerateAsync_MisInfoSheet_IsProtected()
{
var search = CreateSearchModelWithMisData();
var result = await _service.GenerateAsync(search);
using var stream = new MemoryStream(result);
using var workbook = new XLWorkbook(stream);
var misSheet = workbook.Worksheet("MIS Info");
misSheet.Protection.IsProtected.ShouldBeTrue();
}
[Fact]
public async Task GenerateAsync_InvestigationSheet_IsProtected()
{
var search = CreateSearchModelWithMisData();
var result = await _service.GenerateAsync(search);
using var stream = new MemoryStream(result);
using var workbook = new XLWorkbook(stream);
var investigationSheet = workbook.Worksheet("Investigation");
investigationSheet.Protection.IsProtected.ShouldBeTrue();
}
#endregion
#region Data Validation Tests
[Fact]
public async Task GenerateAsync_SearchResults_ContainsCorrectData()
{
var search = CreateMinimalSearchModel();
var searchResult = CreateSampleSearchResult();
searchResult.WorkOrderNumber = 99999;
searchResult.ItemNumber = "TEST-ITEM-001";
searchResult.LotNumber = "LOT-999";
search.Results.Add(searchResult);
var result = await _service.GenerateAsync(search);
using var stream = new MemoryStream(result);
using var workbook = new XLWorkbook(stream);
var resultsSheet = workbook.Worksheet("Search Results");
// Data should be in row 2 (after header)
resultsSheet.Cell(2, 1).Value.GetNumber().ShouldBe(99999);
resultsSheet.Cell(2, 3).Value.GetText().ShouldBe("LOT-999");
resultsSheet.Cell(2, 4).Value.GetText().ShouldBe("TEST-ITEM-001");
}
[Fact]
public async Task GenerateAsync_MisInfo_ContainsCorrectData()
{
var search = CreateSearchModelWithMisData();
search.MisResults![0].ItemNumber = "MIS-ITEM-001";
search.MisResults[0].MisNumber = "MIS-12345";
var result = await _service.GenerateAsync(search);
using var stream = new MemoryStream(result);
using var workbook = new XLWorkbook(stream);
var misSheet = workbook.Worksheet("MIS Info");
// Data should be in row 2 (after header)
misSheet.Cell(2, 1).Value.GetText().ShouldBe("MIS-ITEM-001");
misSheet.Cell(2, 3).Value.GetText().ShouldBe("MIS-12345");
}
[Fact]
public async Task GenerateAsync_Investigation_ContainsCorrectData()
{
var search = CreateSearchModelWithMisData();
search.MisNonMatchResults![0].WorkOrderNumber = 77777;
search.MisNonMatchResults[0].ItemNumber = "INV-ITEM-001";
var result = await _service.GenerateAsync(search);
using var stream = new MemoryStream(result);
using var workbook = new XLWorkbook(stream);
var investigationSheet = workbook.Worksheet("Investigation");
// Data should be in row 2 (after header)
investigationSheet.Cell(2, 2).Value.GetNumber().ShouldBe(77777);
investigationSheet.Cell(2, 10).Value.GetText().ShouldBe("INV-ITEM-001");
}
#endregion
#region Wrapped Column Tests
[Fact]
public async Task GenerateAsync_MisInfo_TestDescriptionColumn_IsWrapped()
{
var search = CreateSearchModelWithMisData();
var result = await _service.GenerateAsync(search);
using var stream = new MemoryStream(result);
using var workbook = new XLWorkbook(stream);
var misSheet = workbook.Worksheet("MIS Info");
// Find the Test Description column (should be column 15)
var headers = GetHeadersFromSheet(misSheet);
var testDescColIndex = headers.IndexOf("Test Description") + 1;
misSheet.Column(testDescColIndex).Width.ShouldBe(65);
misSheet.Column(testDescColIndex).Style.Alignment.WrapText.ShouldBeTrue();
}
[Fact]
public async Task GenerateAsync_MisInfo_ToolsGaugesColumn_IsWrapped()
{
var search = CreateSearchModelWithMisData();
var result = await _service.GenerateAsync(search);
using var stream = new MemoryStream(result);
using var workbook = new XLWorkbook(stream);
var misSheet = workbook.Worksheet("MIS Info");
var headers = GetHeadersFromSheet(misSheet);
var toolsGaugesColIndex = headers.IndexOf("Tools & Gauges") + 1;
misSheet.Column(toolsGaugesColIndex).Width.ShouldBe(65);
misSheet.Column(toolsGaugesColIndex).Style.Alignment.WrapText.ShouldBeTrue();
}
[Fact]
public async Task GenerateAsync_MisInfo_WorkInstructionsColumn_IsWrapped()
{
var search = CreateSearchModelWithMisData();
var result = await _service.GenerateAsync(search);
using var stream = new MemoryStream(result);
using var workbook = new XLWorkbook(stream);
var misSheet = workbook.Worksheet("MIS Info");
var headers = GetHeadersFromSheet(misSheet);
var workInstructionsColIndex = headers.IndexOf("Work Instructions") + 1;
misSheet.Column(workInstructionsColIndex).Width.ShouldBe(65);
misSheet.Column(workInstructionsColIndex).Style.Alignment.WrapText.ShouldBeTrue();
}
#endregion
#region Large Data Set Tests
[Fact]
public async Task GenerateAsync_LargeDataSet_GeneratesSuccessfully()
{
var search = CreateMinimalSearchModel();
// Add 1000 search results
for (int i = 0; i < 1000; i++)
{
search.Results.Add(new SearchResult
{
WorkOrderNumber = 10000 + i,
ItemNumber = $"ITEM-{i:D5}",
LotNumber = $"LOT-{i:D5}",
Flagged = true
});
}
var result = await _service.GenerateAsync(search);
using var stream = new MemoryStream(result);
using var workbook = new XLWorkbook(stream);
var resultsSheet = workbook.Worksheet("Search Results");
// Should have 1001 rows (1 header + 1000 data rows)
var table = resultsSheet.Tables.First();
table.RowCount().ShouldBe(1001);
}
#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 CreateMinimalSearchModel()
{
return new SearchModel
{
Id = 1,
Name = "Integration Test Search",
UserName = "testuser",
SubmitDt = DateTime.Now.AddHours(-1),
StartDt = DateTime.Now.AddMinutes(-30),
EndDt = DateTime.Now,
ExtractMisData = false,
Results = []
};
}
private static SearchModel CreateSearchModelWithMisData()
{
return new SearchModel
{
Id = 1,
Name = "Integration Test Search with MIS",
UserName = "testuser",
SubmitDt = DateTime.Now.AddHours(-1),
StartDt = DateTime.Now.AddMinutes(-30),
EndDt = DateTime.Now,
ExtractMisData = true,
Results = [
CreateSampleSearchResult()
],
MisResults = [
new MisSearchResult
{
ItemNumber = "ITEM-001",
MisNumber = "MIS-001",
RevId = "A",
ItemDescription = "Test Item Description",
Status = "Released",
BranchCode = "001",
JobStepSequenceNumber = 10,
TestDescription = "Sample test description",
ToolsGauges = "Sample tools and gauges",
WorkInstructions = "Sample work instructions"
}
],
MisNonMatchResults = [
new MisNonMatchSearchResult
{
WorkOrderNumber = 12345,
ItemNumber = "ITEM-001",
WorkCenterCode = "WC01",
WorkOrderStartDate = DateTime.Now.AddDays(-7),
JobStepNumber = 10,
JobStepDescription = "Test job step",
FunctionCode = "FC01",
RoutingType = "M"
}
]
};
}
private static SearchResult CreateSampleSearchResult()
{
return 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 operation",
StepUpdateDt = DateTime.Now.AddDays(-1),
StatusCode = "50",
StatusDescription = "In Progress",
StatusUpdateDt = DateTime.Now.AddDays(-1),
Flagged = true
};
}
#endregion
}
@@ -0,0 +1,226 @@
using ClosedXML.Excel;
using JdeScoping.ExcelIO.Configuration;
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;
public class ExcelExportServiceTests
{
private readonly ExcelExportService _service;
private readonly ILogger<ExcelExportService> _logger;
private readonly IOptions<ExcelExportOptions> _options;
public ExcelExportServiceTests()
{
_logger = Substitute.For<ILogger<ExcelExportService>>();
_options = Options.Create(new ExcelExportOptions
{
CriteriaSheetPassword = "TestCriteriaPass",
DataSheetPassword = "TestDataPass"
});
var cache = new OutputColumnCache();
var tableWriter = new AttributeTableWriter(cache);
var criteriaGenerator = new CriteriaSheetGenerator(_options, tableWriter);
_service = new ExcelExportService(_logger, _options, criteriaGenerator, tableWriter);
}
[Fact]
public async Task GenerateAsync_ReturnsValidExcelBytes()
{
var search = CreateMinimalSearchModel();
var result = await _service.GenerateAsync(search);
result.ShouldNotBeNull();
result.Length.ShouldBeGreaterThan(0);
// Verify it's a valid Excel file
using var stream = new MemoryStream(result);
using var workbook = new XLWorkbook(stream);
workbook.Worksheets.Count.ShouldBeGreaterThanOrEqualTo(2);
}
[Fact]
public async Task GenerateAsync_CreatesSearchCriteriaSheet()
{
var search = CreateMinimalSearchModel();
var result = await _service.GenerateAsync(search);
using var stream = new MemoryStream(result);
using var workbook = new XLWorkbook(stream);
workbook.Worksheets.TryGetWorksheet("Search Criteria", out var criteriaSheet).ShouldBeTrue();
criteriaSheet.ShouldNotBeNull();
}
[Fact]
public async Task GenerateAsync_CreatesSearchResultsSheet()
{
var search = CreateMinimalSearchModel();
var result = await _service.GenerateAsync(search);
using var stream = new MemoryStream(result);
using var workbook = new XLWorkbook(stream);
workbook.Worksheets.TryGetWorksheet("Search Results", out var resultsSheet).ShouldBeTrue();
resultsSheet.ShouldNotBeNull();
}
[Fact]
public async Task GenerateAsync_WithMisData_CreatesMisInfoSheet()
{
var search = CreateSearchModelWithMisData();
var result = await _service.GenerateAsync(search);
using var stream = new MemoryStream(result);
using var workbook = new XLWorkbook(stream);
workbook.Worksheets.Count.ShouldBe(4); // Criteria, Results, MIS Info, Investigation
workbook.Worksheets.TryGetWorksheet("MIS Info", out var misSheet).ShouldBeTrue();
misSheet.ShouldNotBeNull();
}
[Fact]
public async Task GenerateAsync_WithMisData_CreatesInvestigationSheet()
{
var search = CreateSearchModelWithMisData();
var result = await _service.GenerateAsync(search);
using var stream = new MemoryStream(result);
using var workbook = new XLWorkbook(stream);
workbook.Worksheets.TryGetWorksheet("Investigation", out var investigationSheet).ShouldBeTrue();
investigationSheet.ShouldNotBeNull();
}
[Fact]
public async Task GenerateAsync_WithoutMisData_DoesNotCreateMisSheets()
{
var search = CreateMinimalSearchModel();
search.ExtractMisData = false;
var result = await _service.GenerateAsync(search);
using var stream = new MemoryStream(result);
using var workbook = new XLWorkbook(stream);
workbook.Worksheets.Count.ShouldBe(2); // Only Criteria and Results
}
[Fact]
public async Task GenerateAsync_CancellationRequested_ThrowsOperationCanceled()
{
var search = CreateMinimalSearchModel();
var cts = new CancellationTokenSource();
cts.Cancel();
await Should.ThrowAsync<OperationCanceledException>(
() => _service.GenerateAsync(search, cts.Token));
}
[Fact]
public async Task GenerateAsync_CriteriaSheet_ContainsSearchName()
{
var search = CreateMinimalSearchModel();
search.Name = "Test Search Name";
var result = await _service.GenerateAsync(search);
using var stream = new MemoryStream(result);
using var workbook = new XLWorkbook(stream);
var criteriaSheet = workbook.Worksheet("Search Criteria");
criteriaSheet.Cell(1, 2).Value.GetText().ShouldBe("Test Search Name");
}
[Fact]
public async Task GenerateAsync_CriteriaSheet_ContainsUserName()
{
var search = CreateMinimalSearchModel();
search.UserName = "testuser";
var result = await _service.GenerateAsync(search);
using var stream = new MemoryStream(result);
using var workbook = new XLWorkbook(stream);
var criteriaSheet = workbook.Worksheet("Search Criteria");
criteriaSheet.Cell(2, 2).Value.GetText().ShouldBe("testuser");
}
[Fact]
public async Task GenerateAsync_ResultsSheet_ContainsResultData()
{
var search = CreateMinimalSearchModel();
search.Results.Add(new SearchResult
{
WorkOrderNumber = 12345,
ItemNumber = "ITEM-001",
LotNumber = "LOT-001",
Flagged = true
});
var result = await _service.GenerateAsync(search);
using var stream = new MemoryStream(result);
using var workbook = new XLWorkbook(stream);
var resultsSheet = workbook.Worksheet("Search Results");
// Check header row
resultsSheet.Cell(1, 1).Value.GetText().ShouldBe("Work Order Number");
// Check data row
resultsSheet.Cell(2, 1).Value.GetNumber().ShouldBe(12345);
}
private static SearchModel CreateMinimalSearchModel()
{
return new SearchModel
{
Id = 1,
Name = "Test Search",
UserName = "testuser",
SubmitDt = DateTime.Now.AddHours(-1),
StartDt = DateTime.Now.AddMinutes(-30),
EndDt = DateTime.Now,
ExtractMisData = false,
Results = []
};
}
private static SearchModel CreateSearchModelWithMisData()
{
return new SearchModel
{
Id = 1,
Name = "Test Search with MIS",
UserName = "testuser",
SubmitDt = DateTime.Now.AddHours(-1),
StartDt = DateTime.Now.AddMinutes(-30),
EndDt = DateTime.Now,
ExtractMisData = true,
Results = [
new SearchResult { WorkOrderNumber = 12345, Flagged = true }
],
MisResults = [
new MisSearchResult { ItemNumber = "ITEM-001", MisNumber = "MIS-001" }
],
MisNonMatchResults = [
new MisNonMatchSearchResult { WorkOrderNumber = 12345, ItemNumber = "ITEM-001" }
]
};
}
}
@@ -0,0 +1,80 @@
using ClosedXML.Excel;
using JdeScoping.ExcelIO.Formatting;
using Shouldly;
using Xunit;
namespace JdeScoping.ExcelIO.Tests;
public class HeaderFormatterTests
{
[Fact]
public void ApplyHeaderFormat_Cell_AppliesCorrectStyling()
{
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Test");
var cell = worksheet.Cell(1, 1);
HeaderFormatter.ApplyHeaderFormat(cell, "Test Header");
cell.Value.GetText().ShouldBe("Test Header");
cell.Style.Font.Bold.ShouldBeTrue();
cell.Style.Alignment.Horizontal.ShouldBe(XLAlignmentHorizontalValues.Center);
cell.Style.Fill.BackgroundColor.ShouldBe(XLColor.Gainsboro);
}
[Fact]
public void ApplyHeaderFormat_Cell_WithoutText_AppliesOnlyStyling()
{
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Test");
var cell = worksheet.Cell(1, 1);
cell.Value = "Original";
HeaderFormatter.ApplyHeaderFormat(cell);
cell.Value.GetText().ShouldBe("Original");
cell.Style.Font.Bold.ShouldBeTrue();
}
[Fact]
public void ApplyHeaderFormat_Range_AppliesCorrectStyling()
{
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Test");
var range = worksheet.Range(1, 1, 1, 3);
HeaderFormatter.ApplyHeaderFormat(range, "Header", merge: false);
range.FirstCell().Value.GetText().ShouldBe("Header");
foreach (var cell in range.Cells())
{
cell.Style.Font.Bold.ShouldBeTrue();
cell.Style.Fill.BackgroundColor.ShouldBe(XLColor.Gainsboro);
}
}
[Fact]
public void ApplyHeaderFormat_Range_WithMerge_MergesCells()
{
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Test");
var range = worksheet.Range(1, 1, 1, 3);
HeaderFormatter.ApplyHeaderFormat(range, "Merged Header", merge: true);
range.IsMerged().ShouldBeTrue();
range.FirstCell().Value.GetText().ShouldBe("Merged Header");
}
[Fact]
public void ApplyHeaderFormat_Range_WithoutMerge_DoesNotMergeCells()
{
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Test");
var range = worksheet.Range(1, 1, 1, 3);
HeaderFormatter.ApplyHeaderFormat(range, "Not Merged", merge: false);
range.IsMerged().ShouldBeFalse();
}
}
@@ -0,0 +1,100 @@
using JdeScoping.ExcelIO.Models.Reporting;
using Shouldly;
using Xunit;
namespace JdeScoping.ExcelIO.Tests;
public class InclusionReasonTests
{
[Fact]
public void InclusionReason_ManuallySpecified_ReturnsManuallySpecified()
{
var result = new SearchResult { ManuallySpecified = true };
result.InclusionReason.ShouldBe("ManuallySpecified");
}
[Fact]
public void InclusionReason_Flagged_ReturnsFlagged()
{
var result = new SearchResult { Flagged = true };
result.InclusionReason.ShouldBe("Flagged");
}
[Fact]
public void InclusionReason_CardexAndPartsList_ReturnsComponentUsageBoth()
{
var result = new SearchResult { Cardex = true, PartsList = true };
result.InclusionReason.ShouldBe("ComponentUsage (CARDEX + Parts List)");
}
[Fact]
public void InclusionReason_CardexOnly_ReturnsComponentUsageCardex()
{
var result = new SearchResult { Cardex = true, PartsList = false };
result.InclusionReason.ShouldBe("ComponentUsage (CARDEX)");
}
[Fact]
public void InclusionReason_PartsListOnly_ReturnsComponentUsagePartsList()
{
var result = new SearchResult { Cardex = false, PartsList = true };
result.InclusionReason.ShouldBe("ComponentUsage (Parts List)");
}
[Fact]
public void InclusionReason_SplitOrder_ReturnsSplitOrder()
{
var result = new SearchResult { SplitOrder = true };
result.InclusionReason.ShouldBe("Split order");
}
[Fact]
public void InclusionReason_NoFlags_ReturnsUnknown()
{
var result = new SearchResult();
result.InclusionReason.ShouldBe("UNKNOWN");
}
[Fact]
public void InclusionReason_ManuallySpecified_TakesPrecedenceOverFlagged()
{
var result = new SearchResult
{
ManuallySpecified = true,
Flagged = true
};
result.InclusionReason.ShouldBe("ManuallySpecified");
}
[Fact]
public void InclusionReason_Flagged_TakesPrecedenceOverCardex()
{
var result = new SearchResult
{
Flagged = true,
Cardex = true
};
result.InclusionReason.ShouldBe("Flagged");
}
[Fact]
public void InclusionReason_Cardex_TakesPrecedenceOverSplitOrder()
{
var result = new SearchResult
{
Cardex = true,
SplitOrder = true
};
result.InclusionReason.ShouldBe("ComponentUsage (CARDEX)");
}
}
@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Shouldly" Version="4.3.0" />
<PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Include="coverlet.collector" Version="6.0.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\JdeScoping.ExcelIO\JdeScoping.ExcelIO.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,500 @@
using ClosedXML.Excel;
using JdeScoping.ExcelIO.Configuration;
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 = 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
}
@@ -0,0 +1,100 @@
using JdeScoping.ExcelIO.Attributes;
using JdeScoping.ExcelIO.Helpers;
using Shouldly;
using Xunit;
namespace JdeScoping.ExcelIO.Tests;
public class OutputColumnCacheTests
{
private readonly OutputColumnCache _cache = new();
[OutputTable(TabName = "Test Table", TableName = "Test_Table")]
private class TestModel
{
[OutputColumn(Order = 30, HeaderText = "Column C")]
public string ColumnC { get; set; } = string.Empty;
[OutputColumn(Order = 10, HeaderText = "Column A")]
public string ColumnA { get; set; } = string.Empty;
[OutputColumn(Order = 20, HeaderText = "Column B")]
public string ColumnB { get; set; } = string.Empty;
public string NonOutputColumn { get; set; } = string.Empty;
}
private class TieBreakModel
{
[OutputColumn(Order = 10, HeaderText = "Zebra")]
public string Zebra { get; set; } = string.Empty;
[OutputColumn(Order = 10, HeaderText = "Apple")]
public string Apple { get; set; } = string.Empty;
[OutputColumn(Order = 10, HeaderText = "Mango")]
public string Mango { get; set; } = string.Empty;
}
private class EmptyModel
{
public string NoAttributes { get; set; } = string.Empty;
}
[Fact]
public void GetColumns_ReturnsColumnsOrderedByOrderProperty()
{
var columns = _cache.GetColumns<TestModel>();
columns.Count.ShouldBe(3);
columns[0].Attribute.HeaderText.ShouldBe("Column A");
columns[1].Attribute.HeaderText.ShouldBe("Column B");
columns[2].Attribute.HeaderText.ShouldBe("Column C");
}
[Fact]
public void GetColumns_TieBreaksAlphabeticallyByPropertyName()
{
var columns = _cache.GetColumns<TieBreakModel>();
columns.Count.ShouldBe(3);
// All have Order=10, so should be sorted by property name
columns[0].Name.ShouldBe("Apple");
columns[1].Name.ShouldBe("Mango");
columns[2].Name.ShouldBe("Zebra");
}
[Fact]
public void GetColumns_ExcludesPropertiesWithoutAttribute()
{
var columns = _cache.GetColumns<TestModel>();
columns.Count.ShouldBe(3);
columns.ShouldNotContain(c => c.Name == "NonOutputColumn");
}
[Fact]
public void GetColumns_ReturnsEmptyForEmptyModel()
{
var columns = _cache.GetColumns<EmptyModel>();
columns.Count.ShouldBe(0);
}
[Fact]
public void GetColumns_CachesResults()
{
var columns1 = _cache.GetColumns<TestModel>();
var columns2 = _cache.GetColumns<TestModel>();
ReferenceEquals(columns1, columns2).ShouldBeTrue();
}
[Fact]
public void GetColumns_ByType_ReturnsCorrectColumns()
{
var columns = _cache.GetColumns(typeof(TestModel));
columns.Count.ShouldBe(3);
}
}
@@ -0,0 +1,178 @@
using ClosedXML.Excel;
using JdeScoping.ExcelIO.Parsing;
using Shouldly;
using Xunit;
namespace JdeScoping.ExcelIO.Tests.Parsing;
public class ExcelParserServiceTests
{
private readonly ExcelParserService _service = new();
[Fact]
public void ParseWorkOrders_ReturnsWorkOrderNumbers()
{
// Arrange
var excelData = CreateWorkOrderExcel([12345, 67890, 11111]);
// Act
using var stream = new MemoryStream(excelData);
var result = _service.ParseWorkOrders(stream);
// Assert
result.Count.ShouldBe(3);
result.ShouldContain(12345);
result.ShouldContain(67890);
result.ShouldContain(11111);
}
[Fact]
public void ParseWorkOrders_SkipsInvalidNumbers()
{
// Arrange
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Sheet1");
worksheet.Cell(1, 1).Value = "Work Order";
worksheet.Cell(2, 1).Value = "12345";
worksheet.Cell(3, 1).Value = "not-a-number";
worksheet.Cell(4, 1).Value = "67890";
using var ms = new MemoryStream();
workbook.SaveAs(ms);
ms.Position = 0;
// Act
var result = _service.ParseWorkOrders(ms);
// Assert
result.Count.ShouldBe(2);
}
[Fact]
public void ParseItems_ReturnsItemNumbers()
{
// Arrange
var excelData = CreateItemExcel(["ITEM-001", "ITEM-002"]);
// Act
using var stream = new MemoryStream(excelData);
var result = _service.ParseItems(stream);
// Assert
result.Count.ShouldBe(2);
result.ShouldContain("ITEM-001");
result.ShouldContain("ITEM-002");
}
[Fact]
public void ParseComponentLots_ReturnsLotViewModels()
{
// Arrange
var excelData = CreateComponentLotExcel([("LOT001", "ITEM-001"), ("LOT002", "ITEM-002")]);
// Act
using var stream = new MemoryStream(excelData);
var result = _service.ParseComponentLots(stream);
// Assert
result.Count.ShouldBe(2);
result[0].LotNumber.ShouldBe("LOT001");
result[0].ItemNumber.ShouldBe("ITEM-001");
}
[Fact]
public void ParsePartOperations_ReturnsPartOperations()
{
// Arrange
var excelData = CreatePartOperationExcel([("ITEM-001", "100", "MIS001", "A")]);
// Act
using var stream = new MemoryStream(excelData);
var result = _service.ParsePartOperations(stream);
// Assert
result.Count.ShouldBe(1);
result[0].ItemNumber.ShouldBe("ITEM-001");
result[0].OperationNumber.ShouldBe("100");
result[0].MisNumber.ShouldBe("MIS001");
result[0].MisRevision.ShouldBe("A");
}
[Fact]
public void ParsePartOperations_TruncatesDecimalOperationNumbers()
{
// Arrange
var excelData = CreatePartOperationExcel([("ITEM-001", "100.5", "MIS001", "A")]);
// Act
using var stream = new MemoryStream(excelData);
var result = _service.ParsePartOperations(stream);
// Assert
result[0].OperationNumber.ShouldBe("100");
}
private static byte[] CreateWorkOrderExcel(long[] workOrderNumbers)
{
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Sheet1");
worksheet.Cell(1, 1).Value = "Work Order Number";
for (var i = 0; i < workOrderNumbers.Length; i++)
{
worksheet.Cell(i + 2, 1).Value = workOrderNumbers[i];
}
using var stream = new MemoryStream();
workbook.SaveAs(stream);
return stream.ToArray();
}
private static byte[] CreateItemExcel(string[] itemNumbers)
{
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Sheet1");
worksheet.Cell(1, 1).Value = "Item Number";
for (var i = 0; i < itemNumbers.Length; i++)
{
worksheet.Cell(i + 2, 1).Value = itemNumbers[i];
}
using var stream = new MemoryStream();
workbook.SaveAs(stream);
return stream.ToArray();
}
private static byte[] CreateComponentLotExcel((string LotNumber, string ItemNumber)[] lots)
{
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Sheet1");
worksheet.Cell(1, 1).Value = "Lot Number";
worksheet.Cell(1, 2).Value = "Item Number";
for (var i = 0; i < lots.Length; i++)
{
worksheet.Cell(i + 2, 1).Value = lots[i].LotNumber;
worksheet.Cell(i + 2, 2).Value = lots[i].ItemNumber;
}
using var stream = new MemoryStream();
workbook.SaveAs(stream);
return stream.ToArray();
}
private static byte[] CreatePartOperationExcel((string ItemNumber, string OpNumber, string MisNumber, string MisRevision)[] operations)
{
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Sheet1");
worksheet.Cell(1, 1).Value = "Item Number";
worksheet.Cell(1, 2).Value = "Operation Number";
worksheet.Cell(1, 3).Value = "MIS Number";
worksheet.Cell(1, 4).Value = "MIS Revision";
for (var i = 0; i < operations.Length; i++)
{
worksheet.Cell(i + 2, 1).Value = operations[i].ItemNumber;
worksheet.Cell(i + 2, 2).Value = operations[i].OpNumber;
worksheet.Cell(i + 2, 3).Value = operations[i].MisNumber;
worksheet.Cell(i + 2, 4).Value = operations[i].MisRevision;
}
using var stream = new MemoryStream();
workbook.SaveAs(stream);
return stream.ToArray();
}
}
@@ -0,0 +1,96 @@
using ClosedXML.Excel;
using JdeScoping.ExcelIO.Templates;
using Shouldly;
using Xunit;
namespace JdeScoping.ExcelIO.Tests.Templates;
public class ExcelTemplateServiceTests
{
private readonly ExcelTemplateService _service = new();
[Fact]
public void GenerateSingleColumn_CreatesValidExcel()
{
// Arrange
var data = new[] { 12345L, 67890L };
// Act
var result = _service.GenerateSingleColumn(data, "Work Order Number");
// Assert
result.ShouldNotBeNull();
result.Length.ShouldBeGreaterThan(0);
// Verify content
using var stream = new MemoryStream(result);
using var workbook = new XLWorkbook(stream);
var worksheet = workbook.Worksheet(1);
worksheet.Cell(1, 1).GetString().ShouldBe("Work Order Number");
worksheet.Cell(2, 1).GetString().ShouldBe("12345");
worksheet.Cell(3, 1).GetString().ShouldBe("67890");
}
[Fact]
public void GenerateMultiColumn_CreatesValidExcel()
{
// Arrange
var data = new[]
{
new object?[] { "ITEM-001", "Description 1" },
new object?[] { "ITEM-002", "Description 2" }
};
var headers = new[] { "Item Number", "Description" };
// Act
var result = _service.GenerateMultiColumn(data, headers);
// Assert
result.ShouldNotBeNull();
result.Length.ShouldBeGreaterThan(0);
// Verify content
using var stream = new MemoryStream(result);
using var workbook = new XLWorkbook(stream);
var worksheet = workbook.Worksheet(1);
worksheet.Cell(1, 1).GetString().ShouldBe("Item Number");
worksheet.Cell(1, 2).GetString().ShouldBe("Description");
worksheet.Cell(2, 1).GetString().ShouldBe("ITEM-001");
worksheet.Cell(2, 2).GetString().ShouldBe("Description 1");
}
[Fact]
public void GenerateSingleColumn_HandlesEmptyData()
{
// Act
var result = _service.GenerateSingleColumn(Array.Empty<string>(), "Header");
// Assert
result.ShouldNotBeNull();
result.Length.ShouldBeGreaterThan(0);
}
[Fact]
public void GenerateMultiColumn_HandlesNullValues()
{
// Arrange
var data = new[]
{
new object?[] { "ITEM-001", null }
};
var headers = new[] { "Item", "Value" };
// Act
var result = _service.GenerateMultiColumn(data, headers);
// Assert
result.ShouldNotBeNull();
using var stream = new MemoryStream(result);
using var workbook = new XLWorkbook(stream);
var worksheet = workbook.Worksheet(1);
worksheet.Cell(2, 2).GetString().ShouldBe(string.Empty);
}
}
@@ -0,0 +1,79 @@
using ClosedXML.Excel;
using JdeScoping.ExcelIO.Formatting;
using Shouldly;
using Xunit;
namespace JdeScoping.ExcelIO.Tests;
public class WorksheetProtectorTests
{
[Fact]
public void ApplyProtection_ProtectsWorksheet()
{
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Test");
WorksheetProtector.ApplyProtection(worksheet, "TestPassword");
worksheet.Protection.IsProtected.ShouldBeTrue();
}
[Fact]
public void ApplyProtection_AllowsSpecifiedOperations()
{
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Test");
WorksheetProtector.ApplyProtection(worksheet, "TestPassword");
// Check that specified operations are allowed
worksheet.Protection.AllowedElements.HasFlag(XLSheetProtectionElements.DeleteColumns).ShouldBeTrue();
worksheet.Protection.AllowedElements.HasFlag(XLSheetProtectionElements.AutoFilter).ShouldBeTrue();
worksheet.Protection.AllowedElements.HasFlag(XLSheetProtectionElements.FormatCells).ShouldBeTrue();
worksheet.Protection.AllowedElements.HasFlag(XLSheetProtectionElements.FormatColumns).ShouldBeTrue();
worksheet.Protection.AllowedElements.HasFlag(XLSheetProtectionElements.FormatRows).ShouldBeTrue();
worksheet.Protection.AllowedElements.HasFlag(XLSheetProtectionElements.SelectLockedCells).ShouldBeTrue();
worksheet.Protection.AllowedElements.HasFlag(XLSheetProtectionElements.SelectUnlockedCells).ShouldBeTrue();
worksheet.Protection.AllowedElements.HasFlag(XLSheetProtectionElements.EditObjects).ShouldBeTrue();
worksheet.Protection.AllowedElements.HasFlag(XLSheetProtectionElements.Sort).ShouldBeTrue();
}
[Fact]
public void ApplyProtection_DoesNotAllowDeleteRows()
{
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Test");
WorksheetProtector.ApplyProtection(worksheet, "TestPassword");
// DeleteRows should NOT be allowed
worksheet.Protection.AllowedElements.HasFlag(XLSheetProtectionElements.DeleteRows).ShouldBeFalse();
}
[Fact]
public void ApplyCriteriaProtection_ProtectsWorksheet()
{
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Test");
WorksheetProtector.ApplyCriteriaProtection(worksheet, "CriteriaPassword");
worksheet.Protection.IsProtected.ShouldBeTrue();
}
[Fact]
public void UnlockExtensionArea_UnlocksSpecifiedRange()
{
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Test");
// First, set some cells to locked (default)
worksheet.Range(1, 1, 10, 5).Style.Protection.Locked = true;
WorksheetProtector.UnlockExtensionArea(worksheet, 10, 5, 100, 100);
// Extension area should be unlocked
var extensionCell = worksheet.Cell(1, 6);
extensionCell.Style.Protection.Locked.ShouldBeFalse();
}
}