using Microsoft.Extensions.Options; using ZB.MOM.WW.MxGateway.Server.Galaxy; namespace ZB.MOM.WW.MxGateway.Tests.Galaxy; /// /// Covers : the on-disk persistence /// that lets the Galaxy browse cache survive a cold start while the Galaxy /// database is unreachable. /// public sealed class GalaxyHierarchySnapshotStoreTests : IDisposable { private readonly List _tempPaths = []; [Fact] public async Task SaveAsync_ThenTryLoadAsync_RoundTripsRows() { string path = CreateTempPath(); GalaxyHierarchySnapshotStore store = CreateStore(path); GalaxyHierarchySnapshot snapshot = SampleSnapshot(); await store.SaveAsync(snapshot, CancellationToken.None); GalaxyHierarchySnapshot? loaded = await store.TryLoadAsync(CancellationToken.None); Assert.NotNull(loaded); Assert.Equal(snapshot.LastDeployTime, loaded.LastDeployTime); Assert.Equal(snapshot.SavedAt, loaded.SavedAt); GalaxyHierarchyRow row = Assert.Single(loaded.Hierarchy); Assert.Equal(7, row.GobjectId); Assert.Equal("Pump_001", row.TagName); Assert.Equal(["AppPump", "Pump"], row.TemplateChain); Assert.Equal(2, loaded.Attributes.Count); GalaxyAttributeRow withDimension = loaded.Attributes[0]; Assert.Equal("PV", withDimension.AttributeName); Assert.Equal(8, withDimension.ArrayDimension); Assert.True(withDimension.IsAlarm); Assert.Null(loaded.Attributes[1].ArrayDimension); } [Fact] public async Task TryLoadAsync_WhenNoFileExists_ReturnsNull() { GalaxyHierarchySnapshotStore store = CreateStore(CreateTempPath()); Assert.Null(await store.TryLoadAsync(CancellationToken.None)); } [Fact] public async Task SaveAsync_WhenPersistenceDisabled_WritesNothing() { string path = CreateTempPath(); GalaxyHierarchySnapshotStore store = CreateStore(path, persist: false); await store.SaveAsync(SampleSnapshot(), CancellationToken.None); Assert.False(File.Exists(path)); Assert.Null(await store.TryLoadAsync(CancellationToken.None)); } [Fact] public async Task TryLoadAsync_WhenFileIsCorruptJson_ReturnsNull() { string path = CreateTempPath(); await File.WriteAllTextAsync(path, "{ this is not valid json"); GalaxyHierarchySnapshotStore store = CreateStore(path); Assert.Null(await store.TryLoadAsync(CancellationToken.None)); } [Fact] public async Task TryLoadAsync_WhenSchemaVersionUnrecognized_ReturnsNull() { string path = CreateTempPath(); await File.WriteAllTextAsync(path, """{"SchemaVersion":999,"Snapshot":null}"""); GalaxyHierarchySnapshotStore store = CreateStore(path); Assert.Null(await store.TryLoadAsync(CancellationToken.None)); } [Fact] public async Task SaveAsync_OverwritesAnEarlierSnapshot() { string path = CreateTempPath(); GalaxyHierarchySnapshotStore store = CreateStore(path); await store.SaveAsync(SampleSnapshot(), CancellationToken.None); GalaxyHierarchySnapshot second = SampleSnapshot() with { Hierarchy = [], Attributes = [], }; await store.SaveAsync(second, CancellationToken.None); GalaxyHierarchySnapshot? loaded = await store.TryLoadAsync(CancellationToken.None); Assert.NotNull(loaded); Assert.Empty(loaded.Hierarchy); Assert.Empty(loaded.Attributes); } private static GalaxyHierarchySnapshot SampleSnapshot() => new( LastDeployTime: new DateTimeOffset(2026, 5, 20, 9, 30, 0, TimeSpan.Zero), SavedAt: new DateTimeOffset(2026, 5, 20, 9, 31, 0, TimeSpan.Zero), Hierarchy: [ new GalaxyHierarchyRow { GobjectId = 7, TagName = "Pump_001", ContainedName = "Pump", BrowseName = "Pump", CategoryId = 10, TemplateChain = ["AppPump", "Pump"], }, ], Attributes: [ new GalaxyAttributeRow { GobjectId = 7, TagName = "Pump_001", AttributeName = "PV", FullTagReference = "Pump_001.PV[]", MxDataType = 5, DataTypeName = "Float", IsArray = true, ArrayDimension = 8, IsAlarm = true, }, new GalaxyAttributeRow { GobjectId = 7, TagName = "Pump_001", AttributeName = "Mode", FullTagReference = "Pump_001.Mode", MxDataType = 3, DataTypeName = "Integer", ArrayDimension = null, }, ]); private static GalaxyHierarchySnapshotStore CreateStore(string path, bool persist = true) { GalaxyRepositoryOptions options = new() { PersistSnapshot = persist, SnapshotCachePath = path, }; return new GalaxyHierarchySnapshotStore(Options.Create(options)); } private string CreateTempPath() { string path = Path.Combine( Path.GetTempPath(), $"mxgw-galaxy-snapshot-{Guid.NewGuid():N}.json"); _tempPaths.Add(path); return path; } public void Dispose() { foreach (string path in _tempPaths) { try { File.Delete(path); File.Delete(path + ".tmp"); } catch (IOException) { // Best-effort cleanup of test scratch files. } } } }