Files
MeterVault/tests/Integration.Tests/TimescaleFixture.cs
T
schmidt.florian 5977c81002 M2: German CSV importer + reconciliation of all 4 fixtures
- German-dialect scalar parsers in Core (GermanNumber/Money/Date, ValueCell):
  decimal comma, thousands dot, unit suffixes, € currency, both date shapes.
- Declarative MappingProfile + RowClassifier (skip summary/blank/all-zero rows) +
  CsvImporter (CsvHelper) staging readings/events/manual-costs, with auto swap
  detection on register decreases and month-end anchoring for interleaved oil dates.
- Four built-in ReferenceProfiles (Strom/Wasser/Heizöl/Kosten).
- ImportService: commit as revertible import_batch + wholesale consumption recompute
  per affected meter (NormalizationService/MeterConfigFactory), revert by batch.
- Reconciliation tests: all 4 CSVs match the sheet's own columns within tolerance
  (electricity 5 meters + Netz Einsparung, water swap→12, oil tank incl. deliveries +
  burner hours, cost category totals). Commit/revert round-trip verified on Timescale.

69 tests green (53 Core + 16 integration).

Known follow-up (polish): historical imports can contend with the 30-day compression
policy's background job; tests pause it. Consider retry-on-deadlock or deferred
compression for large historical imports in production.

Claude-Session: https://claude.ai/code/session_01WujdMtMJPbxDpDnMeK22rr
2026-07-13 11:35:54 +02:00

54 lines
2.3 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using MeterVault.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;
using Testcontainers.PostgreSql;
namespace MeterVault.Integration.Tests;
/// <summary>
/// Spins up exactly one TimescaleDB container per test run (shared via the "Timescale"
/// collection) and applies migrations once. Tests reuse it and isolate themselves with Respawn.
/// The image tag is pinned: the compression DDL (add_compression_policy) was renamed toward
/// add_columnstore_policy in newer Timescale, so a floating tag would risk breaking migrations.
/// </summary>
public sealed class TimescaleFixture : IAsyncLifetime
{
private readonly PostgreSqlContainer _db = new PostgreSqlBuilder("timescale/timescaledb:2.17.2-pg16")
.WithDatabase("metervault")
.WithUsername("metervault")
.WithPassword("metervault")
.Build();
public string ConnectionString => _db.GetConnectionString();
public MeterVaultDbContext CreateContext()
{
var options = new DbContextOptionsBuilder<MeterVaultDbContext>()
.UseNpgsql(ConnectionString, npgsql =>
npgsql.MigrationsAssembly(typeof(MeterVaultDbContext).Assembly.FullName))
.UseSnakeCaseNamingConvention()
.Options;
return new MeterVaultDbContext(options);
}
public async Task InitializeAsync()
{
await _db.StartAsync();
await using var ctx = CreateContext();
await ctx.Database.MigrateAsync();
// The fixtures are historical (20222023), so freshly-inserted chunks immediately fall
// past the 30-day compression horizon. Pause the compression job's scheduler so its
// background worker can't deadlock a test's import transaction. The policy still exists
// (SchemaTests verifies that); only its scheduling is disabled.
await ctx.Database.ExecuteSqlRawAsync(
"SELECT alter_job(job_id, scheduled => false) FROM timescaledb_information.jobs " +
"WHERE proc_name IN ('policy_compression', 'policy_columnstore');");
}
public async Task DisposeAsync() => await _db.DisposeAsync();
}
/// <summary>Binds the shared <see cref="TimescaleFixture"/> to all tests in the "Timescale" collection.</summary>
[CollectionDefinition("Timescale")]
public sealed class TimescaleCollection : ICollectionFixture<TimescaleFixture>;