48a7f5a825
Five-project Clean Architecture solution (Core/Infrastructure/App + Core.Tests/ Integration.Tests) on .NET 10 with central package management, snake_case EF mapping, and shared build/style config. - Full domain entity set + EF DbContext for the SDD §5.3 schema (singular table names). - InitialSchema migration (relational) + TimescaleHypertables migration (raw SQL: create_hypertable + compression on reading, hypertable on consumption). - App wiring: Serilog (actually wired, unlike MQTTower), DbContext, migrate-on-startup, /healthz. Serves plain HTTP behind a reverse proxy (no HTTPS redirect). - deploy/Dockerfile (2-stage, ICU-capable) + docker-compose (app + timescaledb). - Integration.Tests: shared TimescaleFixture (Testcontainers) — migrations, hypertables, compression policy, and /healthz all verified green (4/4). Claude-Session: https://claude.ai/code/session_01WujdMtMJPbxDpDnMeK22rr
61 lines
2.0 KiB
C#
61 lines
2.0 KiB
C#
using System.Data.Common;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace MeterVault.Integration.Tests;
|
|
|
|
[Collection("Timescale")]
|
|
public sealed class SchemaTests(TimescaleFixture fx)
|
|
{
|
|
[Fact]
|
|
public async Task Migrations_are_applied()
|
|
{
|
|
await using var ctx = fx.CreateContext();
|
|
var applied = (await ctx.Database.GetAppliedMigrationsAsync()).ToList();
|
|
|
|
Assert.Contains(applied, m => m.EndsWith("InitialSchema", StringComparison.Ordinal));
|
|
Assert.Contains(applied, m => m.EndsWith("TimescaleHypertables", StringComparison.Ordinal));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Reading_and_consumption_are_hypertables()
|
|
{
|
|
await using var ctx = fx.CreateContext();
|
|
var conn = ctx.Database.GetDbConnection();
|
|
await conn.OpenAsync();
|
|
|
|
var names = await QueryStringsAsync(conn,
|
|
"SELECT hypertable_name FROM timescaledb_information.hypertables ORDER BY hypertable_name;");
|
|
|
|
Assert.Equal(["consumption", "reading"], names);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Reading_has_a_compression_policy()
|
|
{
|
|
await using var ctx = fx.CreateContext();
|
|
var conn = ctx.Database.GetDbConnection();
|
|
await conn.OpenAsync();
|
|
|
|
// A compression/columnstore policy shows up as a scheduled job against the hypertable.
|
|
var jobs = await QueryStringsAsync(conn,
|
|
"SELECT hypertable_name FROM timescaledb_information.jobs " +
|
|
"WHERE proc_name IN ('policy_compression', 'policy_columnstore') AND hypertable_name = 'reading';");
|
|
|
|
Assert.Contains("reading", jobs);
|
|
}
|
|
|
|
private static async Task<List<string>> QueryStringsAsync(DbConnection conn, string sql)
|
|
{
|
|
await using var cmd = conn.CreateCommand();
|
|
cmd.CommandText = sql;
|
|
var results = new List<string>();
|
|
await using var reader = await cmd.ExecuteReaderAsync();
|
|
while (await reader.ReadAsync())
|
|
{
|
|
results.Add(reader.GetString(0));
|
|
}
|
|
|
|
return results;
|
|
}
|
|
}
|