manometer

This commit is contained in:
2026-04-17 12:00:03 +02:00
parent bec0410ef4
commit eb187cdc15
15 changed files with 1817 additions and 43 deletions
@@ -224,6 +224,96 @@ public class ConfigTransferServiceTests : IDisposable
Assert.Equal("FirstNonEmpty", rule.TransformationType);
}
[Fact]
public async Task ImportJsonAsync_Preserves_CentralSalesRecords()
{
await SeedExistingSecretsAsync();
await using (var db = await _dbFactory.CreateDbContextAsync())
{
db.CentralSalesRecords.Add(new CentralSalesRecord
{
StoredAtUtc = new DateTime(2026, 4, 17, 8, 0, 0, DateTimeKind.Utc),
SiteId = 1,
SourceSystem = "MANUAL_EXCEL",
ExtractionDate = new DateTime(2026, 4, 17),
Tsc = "TRCH",
InvoiceNumber = "INV-1",
PositionOnInvoice = 1,
Material = "MAT-1",
Name = "Material 1",
ProductGroup = "PG",
Quantity = 1m,
SupplierNumber = "SUP-1",
SupplierName = "Supplier 1",
SupplierCountry = "CH",
CustomerNumber = "CUS-1",
CustomerName = "Customer 1",
CustomerCountry = "CH",
CustomerIndustry = "Industry",
StandardCost = 10m,
StandardCostCurrency = "CHF",
PurchaseOrderNumber = "PO-1",
SalesPriceValue = 20m,
SalesCurrency = "CHF",
Incoterms2020 = "EXW",
SalesResponsibleEmployee = "Owner",
InvoiceDate = new DateTime(2026, 4, 17),
OrderDate = new DateTime(2026, 4, 16),
Land = "Schweiz",
DocumentType = "Invoice"
});
await db.SaveChangesAsync();
}
var package = new ConfigTransferPackage
{
IncludesSecrets = false,
SourceSystemDefinitions = BuildStandardSourceSystems(),
Sites =
[
new ConfigTransferSite
{
Key = "site-1",
Schema = "schema_a",
TSC = "TRCH",
Land = "Schweiz",
SourceSystem = "MANUAL_EXCEL",
IsActive = true
}
]
};
await _service.ImportJsonAsync(JsonSerializer.Serialize(package));
await using var verifyDb = await _dbFactory.CreateDbContextAsync();
Assert.Single(verifyDb.CentralSalesRecords);
}
[Fact]
public async Task ImportJsonAsync_Uses_Legacy_ConnectionKind_Fallbacks()
{
var packageJson = """
{
"includesSecrets": false,
"sourceSystemDefinitions": [
{ "code": "SAP", "displayName": "SAP", "isActive": true },
{ "code": "BI1", "displayName": "BI1", "isActive": true },
{ "code": "MANUAL_EXCEL", "displayName": "Manual Excel", "isActive": true }
]
}
""";
await _service.ImportJsonAsync(packageJson);
await using var db = await _dbFactory.CreateDbContextAsync();
var systems = await db.SourceSystemDefinitions.OrderBy(x => x.Code).ToListAsync();
Assert.Equal(SourceSystemConnectionKinds.Hana, Assert.Single(systems, x => x.Code == "BI1").ConnectionKind);
Assert.Equal(SourceSystemConnectionKinds.ManualExcel, Assert.Single(systems, x => x.Code == "MANUAL_EXCEL").ConnectionKind);
Assert.Equal(SourceSystemConnectionKinds.SapGateway, Assert.Single(systems, x => x.Code == "SAP").ConnectionKind);
}
private async Task SeedExportConfigurationAsync()
{
await using var db = await _dbFactory.CreateDbContextAsync();
@@ -381,6 +471,41 @@ public class ConfigTransferServiceTests : IDisposable
await db.SaveChangesAsync();
}
private static List<ConfigTransferSourceSystemDefinition> BuildStandardSourceSystems()
{
return
[
new ConfigTransferSourceSystemDefinition
{
Code = "SAP",
DisplayName = "SAP",
ConnectionKind = SourceSystemConnectionKinds.SapGateway,
IsActive = true
},
new ConfigTransferSourceSystemDefinition
{
Code = "BI1",
DisplayName = "BI1",
ConnectionKind = SourceSystemConnectionKinds.Hana,
IsActive = true
},
new ConfigTransferSourceSystemDefinition
{
Code = "SAGE",
DisplayName = "SAGE",
ConnectionKind = SourceSystemConnectionKinds.Hana,
IsActive = true
},
new ConfigTransferSourceSystemDefinition
{
Code = "MANUAL_EXCEL",
DisplayName = "Manual Excel",
ConnectionKind = SourceSystemConnectionKinds.ManualExcel,
IsActive = true
}
];
}
private sealed class TestDbContextFactory : IDbContextFactory<AppDbContext>
{
private readonly DbContextOptions<AppDbContext> _options;
@@ -0,0 +1,196 @@
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using TrafagSalesExporter.Data;
using TrafagSalesExporter.Services;
namespace TrafagSalesExporter.Tests;
public class DatabaseInitializationServiceTests : IDisposable
{
private readonly SqliteConnection _connection;
private readonly TestDbContextFactory _dbFactory;
public DatabaseInitializationServiceTests()
{
_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);
}
public void Dispose()
{
_connection.Dispose();
}
[Fact]
public async Task InitializeAsync_Migrates_Sites_Without_Shifting_Columns()
{
await PrepareLegacySitesTableAsync();
var service = new DatabaseInitializationService(_dbFactory);
await service.InitializeAsync();
await using var db = await _dbFactory.CreateDbContextAsync();
var site = await db.Sites.SingleAsync();
Assert.Equal("override-user", site.UsernameOverride);
Assert.Equal("override-password", site.PasswordOverride);
Assert.Equal("C:\\exports\\ch", site.LocalExportFolderOverride);
Assert.Equal("C:\\imports\\manual.xlsx", site.ManualImportFilePath);
Assert.Equal("https://sap.example.local/service", site.SapServiceUrl);
Assert.Equal("A_Sales", site.SapEntitySet);
Assert.Equal("[\"A_Sales\",\"A_Orders\"]", site.SapEntitySetsCache);
Assert.Equal(new DateTime(2026, 4, 17, 7, 30, 0, DateTimeKind.Utc), site.ManualImportLastUploadedAtUtc?.ToUniversalTime());
Assert.Equal(new DateTime(2026, 4, 17, 8, 0, 0, DateTimeKind.Utc), site.SapEntitySetsRefreshedAtUtc?.ToUniversalTime());
}
[Fact]
public async Task InitializeAsync_Repairs_Sites_ForeignKey_To_HanaServersRepairOld()
{
await PrepareBrokenHanaServerForeignKeyAsync();
var service = new DatabaseInitializationService(_dbFactory);
await service.InitializeAsync();
await using var db = await _dbFactory.CreateDbContextAsync();
var site = await db.Sites.SingleAsync();
Assert.Null(await Record.ExceptionAsync(() => db.SaveChangesAsync()));
Assert.Equal("schema_a", site.Schema);
var tableSql = await ReadTableSqlAsync("Sites");
Assert.Contains("REFERENCES HanaServers (Id)", tableSql, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("HanaServers_repair_old", tableSql, StringComparison.OrdinalIgnoreCase);
}
private async Task PrepareLegacySitesTableAsync()
{
await using var db = await _dbFactory.CreateDbContextAsync();
await db.Database.ExecuteSqlRawAsync("DELETE FROM Sites;");
await db.Database.ExecuteSqlRawAsync("DELETE FROM HanaServers;");
await db.Database.ExecuteSqlRawAsync("""
INSERT INTO HanaServers (Id, SourceSystem, Name, Host, Port, DatabaseName, UseSsl, ValidateCertificate, AdditionalParams)
VALUES (1, 'SAP', 'SAP', 'hana-host', 30015, 'DB1', 0, 0, '');
""");
await db.Database.ExecuteSqlRawAsync("PRAGMA foreign_keys = OFF;");
await db.Database.ExecuteSqlRawAsync("ALTER TABLE Sites RENAME TO Sites_current;");
await db.Database.ExecuteSqlRawAsync("""
CREATE TABLE Sites (
Id INTEGER NOT NULL CONSTRAINT PK_Sites PRIMARY KEY AUTOINCREMENT,
HanaServerId INTEGER NOT NULL,
Schema TEXT NOT NULL,
TSC TEXT NOT NULL,
Land TEXT NOT NULL,
SourceSystem TEXT NULL,
UsernameOverride TEXT NULL,
PasswordOverride TEXT NULL,
LocalExportFolderOverride TEXT NULL,
ManualImportFilePath TEXT NULL,
ManualImportLastUploadedAtUtc TEXT NULL,
SapServiceUrl TEXT NULL,
SapEntitySet TEXT NULL,
SapEntitySetsCache TEXT NULL,
SapEntitySetsRefreshedAtUtc TEXT NULL,
IsActive INTEGER NOT NULL,
CONSTRAINT FK_Sites_HanaServers_HanaServerId FOREIGN KEY (HanaServerId) REFERENCES HanaServers (Id)
);
""");
await db.Database.ExecuteSqlRawAsync("""
INSERT INTO Sites (
Id, HanaServerId, Schema, TSC, Land, SourceSystem,
UsernameOverride, PasswordOverride, LocalExportFolderOverride, ManualImportFilePath,
ManualImportLastUploadedAtUtc, SapServiceUrl, SapEntitySet, SapEntitySetsCache,
SapEntitySetsRefreshedAtUtc, IsActive
)
VALUES (
1, 1, 'schema_a', 'TRCH', 'Schweiz', 'SAP',
'override-user', 'override-password', 'C:\exports\ch', 'C:\imports\manual.xlsx',
'2026-04-17 07:30:00Z', 'https://sap.example.local/service', 'A_Sales', '["A_Sales","A_Orders"]',
'2026-04-17 08:00:00Z', 1
);
""");
await db.Database.ExecuteSqlRawAsync("DROP TABLE Sites_current;");
await db.Database.ExecuteSqlRawAsync("PRAGMA foreign_keys = ON;");
}
private async Task PrepareBrokenHanaServerForeignKeyAsync()
{
await using var db = await _dbFactory.CreateDbContextAsync();
await db.Database.ExecuteSqlRawAsync("DELETE FROM Sites;");
await db.Database.ExecuteSqlRawAsync("DELETE FROM HanaServers;");
await db.Database.ExecuteSqlRawAsync("PRAGMA foreign_keys = OFF;");
await db.Database.ExecuteSqlRawAsync("ALTER TABLE Sites RENAME TO Sites_current;");
await db.Database.ExecuteSqlRawAsync("""
CREATE TABLE Sites (
Id INTEGER NOT NULL CONSTRAINT PK_Sites PRIMARY KEY AUTOINCREMENT,
HanaServerId INTEGER NULL,
Schema TEXT NOT NULL,
TSC TEXT NOT NULL,
Land TEXT NOT NULL,
SourceSystem TEXT NOT NULL DEFAULT 'SAP',
UsernameOverride TEXT NOT NULL DEFAULT '',
PasswordOverride TEXT NOT NULL DEFAULT '',
LocalExportFolderOverride TEXT NOT NULL DEFAULT '',
ManualImportFilePath TEXT NOT NULL DEFAULT '',
ManualImportLastUploadedAtUtc TEXT NULL,
SapServiceUrl TEXT NOT NULL DEFAULT '',
SapEntitySet TEXT NOT NULL DEFAULT '',
SapEntitySetsCache TEXT NOT NULL DEFAULT '',
SapEntitySetsRefreshedAtUtc TEXT NULL,
IsActive INTEGER NOT NULL,
CONSTRAINT FK_Sites_HanaServers_HanaServerId FOREIGN KEY (HanaServerId) REFERENCES HanaServers_repair_old (Id)
);
""");
await db.Database.ExecuteSqlRawAsync("""
INSERT INTO Sites (
Id, HanaServerId, Schema, TSC, Land, SourceSystem,
UsernameOverride, PasswordOverride, LocalExportFolderOverride, ManualImportFilePath,
ManualImportLastUploadedAtUtc, SapServiceUrl, SapEntitySet, SapEntitySetsCache,
SapEntitySetsRefreshedAtUtc, IsActive
)
VALUES (
1, NULL, 'schema_a', 'TRUK', 'England', 'MANUAL_EXCEL',
'', '', '', '',
NULL, '', '', '',
NULL, 1
);
""");
await db.Database.ExecuteSqlRawAsync("DROP TABLE Sites_current;");
await db.Database.ExecuteSqlRawAsync("PRAGMA foreign_keys = ON;");
}
private async Task<string> ReadTableSqlAsync(string tableName)
{
await using var command = _connection.CreateCommand();
command.CommandText = "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = $tableName;";
command.Parameters.AddWithValue("$tableName", tableName);
return (await command.ExecuteScalarAsync())?.ToString() ?? string.Empty;
}
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));
}
}