M7: release polish — export/import, docs, easy docker push
- Easy docker push (MQTTower ergonomics + the image push it lacks): VERSION file → version-tag.yml (semver-guard auto-tag) → docker-publish.yml (buildx multi-arch → GHCR, registry centralized for one-line retarget to git.finalfactory.de) + ci.yml + build-and-push.ps1. - Verified end to end: deploy/Dockerfile builds; docker compose stack (app + timescaledb) comes up healthy; /healthz and the dashboard respond in-container. - JSON config export/import (ExportService) with id remapping on restore + GET /export, POST /import endpoints; round-trip test preserves meter→type, meter-scoped tariff, category links. - README, HA/Tasmota/MQTT wiring guide (docs/wiring.md), Unraid template. - CLAUDE.md updated to reflect the built codebase. - i18n: locale-aware number/currency formatting (de-DE); full de UI string localization deferred. 95 tests green (56 Core + 39 integration). Claude-Session: https://claude.ai/code/session_01WujdMtMJPbxDpDnMeK22rr
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
using MeterVault.Core.Domain;
|
||||
using MeterVault.Infrastructure.Backup;
|
||||
using MeterVault.Infrastructure.Persistence;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace MeterVault.Integration.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// JSON config export/import (SDD §10): a round-trip through an emptied database preserves the
|
||||
/// relationships (meter → energy type, meter-scoped tariff, category membership) after id remapping.
|
||||
/// </summary>
|
||||
[Collection("Timescale")]
|
||||
public sealed class ExportRoundTripTests(TimescaleFixture fx)
|
||||
{
|
||||
[Fact]
|
||||
public async Task Export_then_import_preserves_relationships()
|
||||
{
|
||||
string json;
|
||||
|
||||
await using (var db = fx.CreateContext())
|
||||
{
|
||||
await WipeAllAsync(db);
|
||||
await DatabaseSeeder.SeedAsync(db);
|
||||
|
||||
var elec = await db.EnergyTypes.FirstAsync(t => t.Key == "electricity");
|
||||
var strom = await db.CostCategories.FirstAsync(c => c.Name == "Strom");
|
||||
var meter = new Meter { Name = "Export Meter", EnergyTypeId = elec.Id, Mode = MeterMode.CumulativeCounter, Unit = "kWh" };
|
||||
db.Meters.Add(meter);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
db.Tariffs.Add(new Tariff { ScopeType = TariffScope.Meter, ScopeId = meter.Id, Component = TariffComponent.UnitPrice, Value = 0.30, Unit = "EUR/kWh", ValidFrom = new DateOnly(2024, 1, 1) });
|
||||
db.CostCategoryMembers.Add(new CostCategoryMember { CategoryId = strom.Id, MeterId = meter.Id });
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
json = await new ExportService(db).ExportJsonAsync();
|
||||
}
|
||||
|
||||
await using (var db = fx.CreateContext())
|
||||
{
|
||||
await WipeAllAsync(db);
|
||||
await new ExportService(db).ImportJsonAsync(json);
|
||||
}
|
||||
|
||||
await using (var db = fx.CreateContext())
|
||||
{
|
||||
var meter = await db.Meters.SingleAsync(m => m.Name == "Export Meter");
|
||||
Assert.True(await db.EnergyTypes.AnyAsync(t => t.Id == meter.EnergyTypeId));
|
||||
|
||||
var tariff = await db.Tariffs.SingleAsync(t => t.ScopeType == TariffScope.Meter);
|
||||
Assert.Equal(meter.Id, tariff.ScopeId);
|
||||
|
||||
var member = await db.CostCategoryMembers.SingleAsync();
|
||||
Assert.Equal(meter.Id, member.MeterId);
|
||||
Assert.True(await db.CostCategories.AnyAsync(c => c.Id == member.CategoryId));
|
||||
|
||||
await WipeAllAsync(db);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task WipeAllAsync(MeterVaultDbContext db)
|
||||
{
|
||||
await db.Consumption.ExecuteDeleteAsync();
|
||||
await db.Readings.ExecuteDeleteAsync();
|
||||
await db.MeterEvents.ExecuteDeleteAsync();
|
||||
await db.ManualCosts.ExecuteDeleteAsync();
|
||||
await db.CostCategoryMembers.ExecuteDeleteAsync();
|
||||
await db.Tariffs.ExecuteDeleteAsync();
|
||||
await db.Tanks.ExecuteDeleteAsync();
|
||||
await db.MeterSources.ExecuteDeleteAsync();
|
||||
await db.Meters.ExecuteDeleteAsync();
|
||||
await db.IngestionEndpoints.ExecuteDeleteAsync();
|
||||
await db.CostCategories.ExecuteDeleteAsync();
|
||||
await db.EnergyTypes.ExecuteDeleteAsync();
|
||||
await db.AppSettings.ExecuteDeleteAsync();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user