unittests

This commit is contained in:
2026-04-16 09:45:10 +02:00
parent a25e5900c7
commit ca91af9682
10 changed files with 996 additions and 3 deletions
@@ -0,0 +1,297 @@
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using System.Text.Json;
using TrafagSalesExporter.Data;
using TrafagSalesExporter.Models;
using TrafagSalesExporter.Services;
namespace TrafagSalesExporter.Tests;
public class ConfigTransferServiceTests : IDisposable
{
private readonly SqliteConnection _connection;
private readonly TestDbContextFactory _dbFactory;
private readonly ConfigTransferService _service;
public ConfigTransferServiceTests()
{
_connection = new SqliteConnection("DataSource=:memory:");
_connection.Open();
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseSqlite(_connection)
.Options;
using (var db = new AppDbContext(options))
{
db.Database.EnsureCreated();
}
_dbFactory = new TestDbContextFactory(options);
_service = new ConfigTransferService(_dbFactory);
}
public void Dispose()
{
_connection.Dispose();
}
[Fact]
public async Task ExportJsonAsync_Excludes_Secrets_When_Requested()
{
await SeedExportConfigurationAsync();
var json = await _service.ExportJsonAsync(includeSecrets: false);
var package = JsonSerializer.Deserialize<ConfigTransferPackage>(json)
?? throw new InvalidOperationException("Package missing.");
Assert.False(package.IncludesSecrets);
Assert.NotNull(package.ExportSettings);
Assert.Null(package.ExportSettings.SapUsername);
Assert.Null(package.ExportSettings.SapPassword);
Assert.NotNull(package.SharePointConfig);
Assert.Null(package.SharePointConfig.ClientSecret);
var server = Assert.Single(package.HanaServers);
Assert.Null(server.Username);
Assert.Null(server.Password);
var site = Assert.Single(package.Sites);
Assert.Null(site.UsernameOverride);
Assert.Null(site.PasswordOverride);
Assert.Equal("C:\\imports\\manual.xlsx", site.ManualImportFilePath);
var rule = Assert.Single(package.FieldTransformationRules);
Assert.Equal("Record", rule.RuleScope);
Assert.Equal("FirstNonEmpty", rule.TransformationType);
}
[Fact]
public async Task ImportJsonAsync_Preserves_Existing_Secrets_When_Import_Excludes_Secrets()
{
await SeedExistingSecretsAsync();
var package = new ConfigTransferPackage
{
IncludesSecrets = false,
SharePointConfig = new ConfigTransferSharePoint
{
SiteUrl = "https://new.sharepoint.local",
ExportFolder = "/new",
TenantId = "new-tenant",
ClientId = "new-client",
ClientSecret = null
},
ExportSettings = new ConfigTransferExportSettings
{
DateFilter = "2026-01-01",
TimerHour = 5,
TimerMinute = 30,
TimerEnabled = false,
DebugLoggingEnabled = true,
LocalSiteExportFolder = "D:\\site",
LocalConsolidatedExportFolder = "D:\\consolidated",
SapUsername = null,
SapPassword = null,
Bi1Username = null,
Bi1Password = null,
SageUsername = null,
SagePassword = null
},
HanaServers =
[
new ConfigTransferHanaServer
{
Key = "server-1",
Name = "Server A",
Host = "hana-a",
Port = 30015,
Username = null,
Password = null,
DatabaseName = "DB1",
UseSsl = true,
ValidateCertificate = false,
AdditionalParams = "x=y"
}
],
Sites =
[
new ConfigTransferSite
{
Key = "site-1",
HanaServerKey = "server-1",
Schema = "schema_a",
TSC = "TRCH",
Land = "Schweiz",
SourceSystem = "MANUAL_EXCEL",
UsernameOverride = null,
PasswordOverride = null,
ManualImportFilePath = "D:\\manual\\trch.xlsx",
ManualImportLastUploadedAtUtc = new DateTime(2026, 4, 16, 8, 0, 0, DateTimeKind.Utc),
IsActive = true
}
],
FieldTransformationRules =
[
new FieldTransformationRule
{
SourceSystem = "MANUAL_EXCEL",
SourceField = "",
TargetField = nameof(SalesRecord.CustomerName),
TransformationType = "FirstNonEmpty",
RuleScope = "Record",
Argument = "CustomerName|SupplierName|Name",
SortOrder = 10,
IsActive = true
}
]
};
await _service.ImportJsonAsync(JsonSerializer.Serialize(package));
await using var db = await _dbFactory.CreateDbContextAsync();
var settings = await db.ExportSettings.SingleAsync();
var sharePoint = await db.SharePointConfigs.SingleAsync();
var server = await db.HanaServers.SingleAsync();
var site = await db.Sites.SingleAsync();
var rule = await db.FieldTransformationRules.SingleAsync();
Assert.Equal("preserved-sap-user", settings.SapUsername);
Assert.Equal("preserved-sap-password", settings.SapPassword);
Assert.Equal("preserved-bi1-user", settings.Bi1Username);
Assert.Equal("preserved-sage-password", settings.SagePassword);
Assert.Equal("preserved-sharepoint-secret", sharePoint.ClientSecret);
Assert.Equal("new-tenant", sharePoint.TenantId);
Assert.Equal("preserved-server-user", server.Username);
Assert.Equal("preserved-server-password", server.Password);
Assert.True(server.UseSsl);
Assert.Equal("preserved-site-user", site.UsernameOverride);
Assert.Equal("preserved-site-password", site.PasswordOverride);
Assert.Equal("D:\\manual\\trch.xlsx", site.ManualImportFilePath);
Assert.Equal("MANUAL_EXCEL", site.SourceSystem);
Assert.Equal("Record", rule.RuleScope);
Assert.Equal("FirstNonEmpty", rule.TransformationType);
}
private async Task SeedExportConfigurationAsync()
{
await using var db = await _dbFactory.CreateDbContextAsync();
db.SharePointConfigs.Add(new SharePointConfig
{
SiteUrl = "https://sharepoint.local",
ExportFolder = "/exports",
TenantId = "tenant",
ClientId = "client",
ClientSecret = "secret"
});
db.ExportSettings.Add(new ExportSettings
{
SapUsername = "sap-user",
SapPassword = "sap-password",
Bi1Username = "bi1-user",
Bi1Password = "bi1-password",
SageUsername = "sage-user",
SagePassword = "sage-password"
});
db.HanaServers.Add(new HanaServer
{
Id = 1,
Name = "Server A",
Host = "hana-a",
Port = 30015,
Username = "server-user",
Password = "server-password",
DatabaseName = "DB1"
});
db.Sites.Add(new Site
{
Id = 1,
HanaServerId = 1,
Schema = "schema_a",
TSC = "TRCH",
Land = "Schweiz",
SourceSystem = "MANUAL_EXCEL",
UsernameOverride = "site-user",
PasswordOverride = "site-password",
ManualImportFilePath = "C:\\imports\\manual.xlsx",
ManualImportLastUploadedAtUtc = new DateTime(2026, 4, 16, 7, 0, 0, DateTimeKind.Utc),
IsActive = true
});
db.FieldTransformationRules.Add(new FieldTransformationRule
{
SourceSystem = "MANUAL_EXCEL",
SourceField = "",
TargetField = nameof(SalesRecord.CustomerName),
TransformationType = "FirstNonEmpty",
RuleScope = "Record",
Argument = "CustomerName|SupplierName|Name",
SortOrder = 10,
IsActive = true
});
await db.SaveChangesAsync();
}
private async Task SeedExistingSecretsAsync()
{
await using var db = await _dbFactory.CreateDbContextAsync();
db.SharePointConfigs.Add(new SharePointConfig
{
SiteUrl = "https://old.sharepoint.local",
ExportFolder = "/old",
TenantId = "old-tenant",
ClientId = "old-client",
ClientSecret = "preserved-sharepoint-secret"
});
db.ExportSettings.Add(new ExportSettings
{
SapUsername = "preserved-sap-user",
SapPassword = "preserved-sap-password",
Bi1Username = "preserved-bi1-user",
Bi1Password = "preserved-bi1-password",
SageUsername = "preserved-sage-user",
SagePassword = "preserved-sage-password"
});
db.HanaServers.Add(new HanaServer
{
Id = 1,
Name = "Server A",
Host = "hana-a",
Port = 30015,
Username = "preserved-server-user",
Password = "preserved-server-password",
DatabaseName = "DB1"
});
db.Sites.Add(new Site
{
Id = 1,
HanaServerId = 1,
Schema = "schema_a",
TSC = "TRCH",
Land = "Schweiz",
SourceSystem = "MANUAL_EXCEL",
UsernameOverride = "preserved-site-user",
PasswordOverride = "preserved-site-password",
IsActive = true
});
await db.SaveChangesAsync();
}
private sealed class TestDbContextFactory : IDbContextFactory<AppDbContext>
{
private readonly DbContextOptions<AppDbContext> _options;
public TestDbContextFactory(DbContextOptions<AppDbContext> options)
{
_options = options;
}
public AppDbContext CreateDbContext() => new(_options);
public Task<AppDbContext> CreateDbContextAsync(CancellationToken cancellationToken = default)
=> Task.FromResult(new AppDbContext(_options));
}
}
@@ -0,0 +1,195 @@
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using TrafagSalesExporter.Data;
using TrafagSalesExporter.Models;
using TrafagSalesExporter.Services;
namespace TrafagSalesExporter.Tests;
public class ManagementCockpitServiceTests : IDisposable
{
private readonly SqliteConnection _connection;
private readonly TestDbContextFactory _dbFactory;
private readonly ManagementCockpitService _service;
public ManagementCockpitServiceTests()
{
_connection = new SqliteConnection("DataSource=:memory:");
_connection.Open();
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseSqlite(_connection)
.Options;
using (var db = new AppDbContext(options))
{
db.Database.EnsureCreated();
if (!db.Sites.Any())
{
db.Sites.Add(new Site
{
Id = 1,
HanaServerId = null,
Schema = "test",
TSC = "TEST",
Land = "Testland",
SourceSystem = "SAP",
IsActive = true
});
db.SaveChanges();
}
}
_dbFactory = new TestDbContextFactory(options);
_service = new ManagementCockpitService(_dbFactory);
}
public void Dispose()
{
_connection.Dispose();
}
[Fact]
public async Task GetAvailableCentralYearsAsync_Returns_Distinct_Ordered_Years()
{
await SeedCentralRowsAsync(
CreateRow("SAP", "CH", "TRCH", "INV-1", "CHF", 100m, new DateTime(2025, 1, 10)),
CreateRow("SAP", "CH", "TRCH", "INV-2", "CHF", 200m, new DateTime(2026, 2, 10)),
CreateRow("SAP", "CH", "TRCH", "INV-3", "CHF", 300m, null, new DateTime(2026, 3, 5)));
var years = await _service.GetAvailableCentralYearsAsync();
Assert.Equal([2025, 2026], years);
}
[Fact]
public async Task AnalyzeCentralAsync_Uses_InvoiceDate_Or_ExtractionDate_And_Builds_Monthly_Daily_And_Source_Totals()
{
await SeedCentralRowsAsync(
CreateRow("SAP", "Schweiz", "TRCH", "INV-1", "CHF", 100m, new DateTime(2025, 1, 10)),
CreateRow("MANUAL_EXCEL", "Deutschland", "TRDE", "INV-2", "EUR", 50m, new DateTime(2025, 1, 11)),
CreateRow("SAP", "Deutschland", "TRDE", "INV-3", "EUR", 25m, null, new DateTime(2025, 1, 12)),
CreateRow("SAP", "Schweiz", "TRCH", "INV-4", "CHF", 70m, new DateTime(2026, 2, 5)));
var result = await _service.AnalyzeCentralAsync(2025, 1);
Assert.Equal(2025, result.Filter.Year);
Assert.Equal(1, result.Filter.Month);
Assert.Equal(3, result.Summary.RowCount);
Assert.Equal(3, result.Summary.InvoiceCount);
Assert.Equal(2, result.Summary.SiteCount);
Assert.Equal(2, result.Summary.CountryCount);
Assert.Equal(2, result.Summary.CurrencyCount);
Assert.Equal(new DateTime(2025, 1, 10), result.Summary.PeriodStart);
Assert.Equal(new DateTime(2025, 1, 12), result.Summary.PeriodEnd);
var yearly2025Chf = Assert.Single(result.YearlyTotals, x => x.Year == 2025 && x.Currency == "CHF");
Assert.Equal(100m, yearly2025Chf.SalesValue);
var yearly2025Eur = Assert.Single(result.YearlyTotals, x => x.Year == 2025 && x.Currency == "EUR");
Assert.Equal(75m, yearly2025Eur.SalesValue);
var januaryChf = Assert.Single(result.MonthlyTotals, x => x.Label == "2025-01" && x.Currency == "CHF");
Assert.Equal(100m, januaryChf.SalesValue);
var januaryEur = Assert.Single(result.MonthlyTotals, x => x.Label == "2025-01" && x.Currency == "EUR");
Assert.Equal(75m, januaryEur.SalesValue);
Assert.Equal(3, result.DailyTotals.Count);
Assert.Contains(result.DailyTotals, x => x.Label == "2025-01-12" && x.Currency == "EUR" && x.SalesValue == 25m);
var sapTotal = Assert.Single(result.SourceSystemTotals, x => x.Label == "SAP" && x.Currency == "CHF");
Assert.Equal(100m, sapTotal.SalesValue);
var manualTotal = Assert.Single(result.SourceSystemTotals, x => x.Label == "MANUAL_EXCEL" && x.Currency == "EUR");
Assert.Equal(50m, manualTotal.SalesValue);
var germanyEur = Assert.Single(result.CountryTotals, x => x.Label == "Deutschland" && x.Currency == "EUR");
Assert.Equal(75m, germanyEur.SalesValue);
Assert.Equal(2, germanyEur.InvoiceCount);
}
[Fact]
public async Task AnalyzeCentralAsync_With_Year_Only_Does_Not_Build_DailyTotals()
{
await SeedCentralRowsAsync(
CreateRow("SAP", "Schweiz", "TRCH", "INV-1", "CHF", 100m, new DateTime(2025, 1, 10)),
CreateRow("SAP", "Schweiz", "TRCH", "INV-2", "CHF", 150m, new DateTime(2025, 2, 10)));
var result = await _service.AnalyzeCentralAsync(2025, null);
Assert.Empty(result.DailyTotals);
Assert.Equal(2, result.MonthlyTotals.Count);
}
[Fact]
public async Task AnalyzeCentralAsync_Throws_When_No_Rows_Exist_For_Selected_Period()
{
await SeedCentralRowsAsync(
CreateRow("SAP", "Schweiz", "TRCH", "INV-1", "CHF", 100m, new DateTime(2025, 1, 10)));
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => _service.AnalyzeCentralAsync(2026, 1));
Assert.Contains("gewählten Zeitraum", ex.Message);
}
private async Task SeedCentralRowsAsync(params CentralSalesRecord[] rows)
{
await using var db = await _dbFactory.CreateDbContextAsync();
db.CentralSalesRecords.RemoveRange(db.CentralSalesRecords);
await db.SaveChangesAsync();
db.CentralSalesRecords.AddRange(rows);
await db.SaveChangesAsync();
}
private static CentralSalesRecord CreateRow(string sourceSystem, string land, string tsc, string invoiceNumber, string currency, decimal salesValue, DateTime? invoiceDate, DateTime? extractionDate = null)
{
return new CentralSalesRecord
{
SiteId = 1,
StoredAtUtc = DateTime.UtcNow,
SourceSystem = sourceSystem,
ExtractionDate = extractionDate ?? invoiceDate ?? DateTime.UtcNow.Date,
Tsc = tsc,
InvoiceNumber = invoiceNumber,
PositionOnInvoice = 1,
Material = "MAT",
Name = "Article",
ProductGroup = "PG",
Quantity = 1m,
SupplierNumber = "SUP",
SupplierName = "Supplier",
SupplierCountry = "CH",
CustomerNumber = "CUS",
CustomerName = "Customer",
CustomerCountry = "CH",
CustomerIndustry = "Industry",
StandardCost = 1m,
StandardCostCurrency = currency,
PurchaseOrderNumber = "PO",
SalesPriceValue = salesValue,
SalesCurrency = currency,
Incoterms2020 = "DAP",
SalesResponsibleEmployee = "Alice",
InvoiceDate = invoiceDate,
OrderDate = invoiceDate?.AddDays(-2),
Land = land,
DocumentType = "Invoice"
};
}
private sealed class TestDbContextFactory : IDbContextFactory<AppDbContext>
{
private readonly DbContextOptions<AppDbContext> _options;
public TestDbContextFactory(DbContextOptions<AppDbContext> options)
{
_options = options;
}
public AppDbContext CreateDbContext() => new(_options);
public Task<AppDbContext> CreateDbContextAsync(CancellationToken cancellationToken = default)
=> Task.FromResult(new AppDbContext(_options));
}
}
@@ -0,0 +1,221 @@
using ClosedXML.Excel;
using TrafagSalesExporter.Models;
using TrafagSalesExporter.Services;
namespace TrafagSalesExporter.Tests;
public class ManualExcelImportServiceTests
{
[Fact]
public async Task ReadSalesRecordsAsync_Reads_Expected_Columns_From_Exporter_Format()
{
var site = new Site
{
TSC = "TRCH",
Land = "Schweiz"
};
var filePath = CreateWorkbook(workbook =>
{
var ws = workbook.Worksheets.Add("Sales");
WriteHeaders(ws);
ws.Cell(2, 1).Value = "15.04.2026 13:45:00";
ws.Cell(2, 2).Value = "TRDE";
ws.Cell(2, 3).Value = "INV-100";
ws.Cell(2, 4).Value = 7;
ws.Cell(2, 5).Value = "MAT-1";
ws.Cell(2, 6).Value = "Pressure Sensor";
ws.Cell(2, 7).Value = "PG-A";
ws.Cell(2, 8).Value = 2.5m;
ws.Cell(2, 9).Value = "SUP-1";
ws.Cell(2, 10).Value = "Supplier";
ws.Cell(2, 11).Value = "DE";
ws.Cell(2, 12).Value = "CUST-1";
ws.Cell(2, 13).Value = "Customer";
ws.Cell(2, 14).Value = "CH";
ws.Cell(2, 15).Value = "Industry";
ws.Cell(2, 16).Value = 10.25m;
ws.Cell(2, 17).Value = "EUR";
ws.Cell(2, 18).Value = "PO-1";
ws.Cell(2, 19).Value = 21.40m;
ws.Cell(2, 20).Value = "EUR";
ws.Cell(2, 21).Value = "DAP";
ws.Cell(2, 22).Value = "Alice";
ws.Cell(2, 23).Value = "14.04.2026";
ws.Cell(2, 24).Value = "10.04.2026";
ws.Cell(2, 25).Value = "Deutschland";
ws.Cell(2, 26).Value = "Invoice";
});
try
{
var service = new ManualExcelImportService();
var rows = await service.ReadSalesRecordsAsync(filePath, site);
var row = Assert.Single(rows);
Assert.Equal("TRDE", row.Tsc);
Assert.Equal("INV-100", row.InvoiceNumber);
Assert.Equal(7, row.PositionOnInvoice);
Assert.Equal("MAT-1", row.Material);
Assert.Equal(2.5m, row.Quantity);
Assert.Equal(10.25m, row.StandardCost);
Assert.Equal(21.40m, row.SalesPriceValue);
Assert.Equal("Deutschland", row.Land);
Assert.Equal("Invoice", row.DocumentType);
Assert.Equal(new DateTime(2026, 4, 14), row.InvoiceDate);
Assert.Equal(new DateTime(2026, 4, 10), row.OrderDate);
}
finally
{
File.Delete(filePath);
}
}
[Fact]
public async Task ReadSalesRecordsAsync_Uses_Site_Fallbacks_When_Tsc_And_Land_Are_Missing()
{
var site = new Site
{
TSC = "TRCH",
Land = "Schweiz"
};
var filePath = CreateWorkbook(workbook =>
{
var ws = workbook.Worksheets.Add("Sales");
WriteHeaders(ws);
ws.Cell(2, 3).Value = "INV-200";
ws.Cell(2, 5).Value = "MAT-2";
});
try
{
var service = new ManualExcelImportService();
var rows = await service.ReadSalesRecordsAsync(filePath, site);
var row = Assert.Single(rows);
Assert.Equal("TRCH", row.Tsc);
Assert.Equal("Schweiz", row.Land);
}
finally
{
File.Delete(filePath);
}
}
[Fact]
public async Task ReadSalesRecordsAsync_Parses_German_Decimal_Text_And_Skips_Empty_Rows()
{
var site = new Site
{
TSC = "TRAT",
Land = "Oesterreich"
};
var filePath = CreateWorkbook(workbook =>
{
var ws = workbook.Worksheets.Add("Sales");
WriteHeaders(ws);
ws.Cell(2, 3).Value = "INV-300";
ws.Cell(2, 8).Value = "1,50";
ws.Cell(2, 16).Value = "3,25";
ws.Cell(2, 19).Value = "7,90";
ws.Cell(3, 1).Value = "";
ws.Cell(3, 2).Value = "";
ws.Cell(3, 3).Value = "";
});
try
{
var service = new ManualExcelImportService();
var rows = await service.ReadSalesRecordsAsync(filePath, site);
var row = Assert.Single(rows);
Assert.Equal(1.50m, row.Quantity);
Assert.Equal(3.25m, row.StandardCost);
Assert.Equal(7.90m, row.SalesPriceValue);
}
finally
{
File.Delete(filePath);
}
}
[Fact]
public async Task ReadSalesRecordsAsync_Throws_When_InvoiceNumber_Header_Is_Missing()
{
var site = new Site
{
TSC = "TRCH",
Land = "Schweiz"
};
var filePath = CreateWorkbook(workbook =>
{
var ws = workbook.Worksheets.Add("Sales");
ws.Cell(1, 1).Value = "TSC";
ws.Cell(1, 2).Value = "Material";
ws.Cell(2, 1).Value = "TRCH";
ws.Cell(2, 2).Value = "MAT-3";
});
try
{
var service = new ManualExcelImportService();
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => service.ReadSalesRecordsAsync(filePath, site));
Assert.Contains("Invoice Number", ex.Message);
}
finally
{
File.Delete(filePath);
}
}
private static string CreateWorkbook(Action<XLWorkbook> fillWorkbook)
{
var filePath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.xlsx");
using var workbook = new XLWorkbook();
fillWorkbook(workbook);
workbook.SaveAs(filePath);
return filePath;
}
private static void WriteHeaders(IXLWorksheet ws)
{
var headers = new[]
{
"extraction date",
"TSC",
"Invoice Number",
"Position on invoice",
"Material",
"Name",
"Product Group",
"Quantity",
"Supplier number",
"Supplier name",
"Supplier country",
"Customer number",
"Customer name",
"Customer country",
"Customer Industry",
"Standard cost",
"Standard Cost Currency",
"Purchase Order number",
"Sales Price/Value",
"Sales Currency",
"Incoterms 2020",
"Sales responsible employee",
"invoice date",
"order date",
"Land",
"Document Type"
};
for (var i = 0; i < headers.Length; i++)
{
ws.Cell(1, i + 1).Value = headers[i];
}
}
}
@@ -0,0 +1,144 @@
using TrafagSalesExporter.Models;
using TrafagSalesExporter.Services;
namespace TrafagSalesExporter.Tests;
public class RecordTransformationServiceTests
{
private readonly RecordTransformationService _service;
public RecordTransformationServiceTests()
{
ITransformationStrategy[] valueStrategies =
[
new CopyTransformationStrategy(),
new UppercaseTransformationStrategy(),
new LowercaseTransformationStrategy(),
new PrefixTransformationStrategy(),
new SuffixTransformationStrategy(),
new ReplaceTransformationStrategy(),
new ConstantTransformationStrategy()
];
IRecordTransformationStrategy[] recordStrategies =
[
new FirstNonEmptyRecordTransformationStrategy()
];
_service = new RecordTransformationService(valueStrategies, recordStrategies);
}
[Fact]
public void Apply_Ignores_Inactive_Rules()
{
var records = new List<SalesRecord>
{
new() { Material = "abc" }
};
var rules = new[]
{
new FieldTransformationRule
{
IsActive = false,
RuleScope = "Value",
SourceField = nameof(SalesRecord.Material),
TargetField = nameof(SalesRecord.Material),
TransformationType = "Uppercase",
SortOrder = 10
}
};
_service.Apply(records, rules);
Assert.Equal("abc", records[0].Material);
}
[Fact]
public void Apply_Uses_SortOrder_For_Multiple_Value_Rules()
{
var records = new List<SalesRecord>
{
new() { Material = "abc" }
};
var rules = new[]
{
new FieldTransformationRule
{
IsActive = true,
RuleScope = "Value",
SourceField = nameof(SalesRecord.Material),
TargetField = nameof(SalesRecord.Material),
TransformationType = "Uppercase",
SortOrder = 20
},
new FieldTransformationRule
{
IsActive = true,
RuleScope = "Value",
SourceField = nameof(SalesRecord.Material),
TargetField = nameof(SalesRecord.Material),
TransformationType = "Prefix",
Argument = "X-",
SortOrder = 10
}
};
_service.Apply(records, rules);
Assert.Equal("X-ABC", records[0].Material);
}
[Fact]
public void Apply_Uses_Record_Strategy_When_RuleScope_Is_Record()
{
var records = new List<SalesRecord>
{
new()
{
CustomerName = "",
SupplierName = "Supplier A",
Name = "Name A"
}
};
var rules = new[]
{
new FieldTransformationRule
{
IsActive = true,
RuleScope = "Record",
TargetField = nameof(SalesRecord.CustomerName),
TransformationType = "FirstNonEmpty",
Argument = $"{nameof(SalesRecord.CustomerName)}|{nameof(SalesRecord.SupplierName)}|{nameof(SalesRecord.Name)}",
SortOrder = 10
}
};
_service.Apply(records, rules);
Assert.Equal("Supplier A", records[0].CustomerName);
}
[Fact]
public void Apply_Converts_Value_To_Target_Type()
{
var records = new List<SalesRecord>
{
new() { Material = "42" }
};
var rules = new[]
{
new FieldTransformationRule
{
IsActive = true,
RuleScope = "Value",
SourceField = nameof(SalesRecord.Material),
TargetField = nameof(SalesRecord.PositionOnInvoice),
TransformationType = "Copy",
SortOrder = 10
}
};
_service.Apply(records, rules);
Assert.Equal(42, records[0].PositionOnInvoice);
}
}
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\TrafagSalesExporter.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,28 @@
using TrafagSalesExporter.Services;
namespace TrafagSalesExporter.Tests;
public class TransformationCatalogTests
{
[Fact]
public void Catalog_Returns_Value_And_Record_Strategies()
{
ITransformationStrategy[] valueStrategies =
[
new CopyTransformationStrategy(),
new ConstantTransformationStrategy()
];
IRecordTransformationStrategy[] recordStrategies =
[
new FirstNonEmptyRecordTransformationStrategy()
];
var catalog = new TransformationCatalog(valueStrategies, recordStrategies);
var all = catalog.GetAll();
Assert.Contains(all, x => x.RuleScope == "Value" && x.Key == "Copy");
Assert.Contains(all, x => x.RuleScope == "Value" && x.Key == "Constant");
Assert.Contains(all, x => x.RuleScope == "Record" && x.Key == "FirstNonEmpty");
}
}
@@ -0,0 +1,50 @@
using TrafagSalesExporter.Models;
using TrafagSalesExporter.Services;
namespace TrafagSalesExporter.Tests;
public class TransformationStrategiesTests
{
[Fact]
public void ReplaceStrategy_Replaces_Text_Using_Argument_Syntax()
{
var strategy = new ReplaceTransformationStrategy();
var result = strategy.Transform("Intercompany Kunde", "Intercompany=>Extern");
Assert.Equal("Extern Kunde", result);
}
[Fact]
public void ConstantStrategy_Returns_Argument_Ignoring_SourceValue()
{
var strategy = new ConstantTransformationStrategy();
var result = strategy.Transform("ignored", "CHF");
Assert.Equal("CHF", result);
}
[Fact]
public void FirstNonEmptyRecordStrategy_Uses_First_Non_Empty_Field_From_Argument_List()
{
var strategy = new FirstNonEmptyRecordTransformationStrategy();
var record = new SalesRecord
{
CustomerName = "",
SupplierName = "Fallback Supplier",
Name = "Article Name"
};
var rule = new FieldTransformationRule
{
RuleScope = "Record",
TargetField = nameof(SalesRecord.CustomerName),
TransformationType = "FirstNonEmpty",
Argument = $"{nameof(SalesRecord.CustomerName)}|{nameof(SalesRecord.SupplierName)}|{nameof(SalesRecord.Name)}"
};
strategy.Transform(record, rule);
Assert.Equal("Fallback Supplier", record.CustomerName);
}
}