using ZB.MOM.WW.GalaxyRepository; using ZB.MOM.WW.GalaxyRepository.Grpc; using ZB.MOM.WW.MxGateway.Server.Dashboard; namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Dashboard; /// /// Unit tests for , the host-side /// projection that turns a shared-library /// into the dashboard's . /// public sealed class DashboardGalaxySummaryProjectorTests { /// /// Verifies that a healthy entry maps status, copies timestamps/counts, and /// groups objects into template usage (ordered by instance count) and /// category counts with resolved names. /// [Fact] public void Project_HealthyEntry_MapsStatusCopiesCountsAndGroupsObjects() { DateTimeOffset queriedAt = new(2026, 6, 25, 10, 0, 0, TimeSpan.Zero); DateTimeOffset successAt = new(2026, 6, 25, 10, 0, 1, TimeSpan.Zero); DateTimeOffset deployAt = new(2026, 6, 24, 8, 30, 0, TimeSpan.Zero); // Two templates (PumpTemplate x2, ValveTemplate x1) and two categories // (10 = UserDefined x3, 13 = Area x1). GalaxyObject area = new() { GobjectId = 1, BrowseName = "AreaA", IsArea = true, CategoryId = 13, }; GalaxyObject pumpA = new() { GobjectId = 2, BrowseName = "Pump01", CategoryId = 10, }; pumpA.TemplateChain.Add("$PumpTemplate"); GalaxyObject pumpB = new() { GobjectId = 3, BrowseName = "Pump02", CategoryId = 10, }; pumpB.TemplateChain.Add("$PumpTemplate"); GalaxyObject valve = new() { GobjectId = 4, BrowseName = "Valve01", CategoryId = 10, }; valve.TemplateChain.Add("$ValveTemplate"); GalaxyObject[] objects = [area, pumpA, pumpB, valve]; GalaxyHierarchyCacheEntry entry = new( Status: GalaxyCacheStatus.Healthy, Sequence: 7, LastQueriedAt: queriedAt, LastSuccessAt: successAt, LastDeployTime: deployAt, LastError: null, Objects: objects, Index: GalaxyHierarchyIndex.Build(objects), ObjectCount: 4, AreaCount: 1, AttributeCount: 12, HistorizedAttributeCount: 5, AlarmAttributeCount: 2); DashboardGalaxySummary summary = DashboardGalaxySummaryProjector.Project(entry); Assert.Equal(DashboardGalaxyStatus.Healthy, summary.Status); Assert.Equal(queriedAt, summary.LastQueriedAt); Assert.Equal(successAt, summary.LastSuccessAt); Assert.Equal(deployAt, summary.LastDeployTime); Assert.Null(summary.LastError); Assert.Equal(4, summary.ObjectCount); Assert.Equal(1, summary.AreaCount); Assert.Equal(12, summary.AttributeCount); Assert.Equal(5, summary.HistorizedAttributeCount); Assert.Equal(2, summary.AlarmAttributeCount); // Templates ordered by instance count descending: PumpTemplate (2) before ValveTemplate (1). Assert.Equal(2, summary.TopTemplates.Count); Assert.Equal("$PumpTemplate", summary.TopTemplates[0].TemplateName); Assert.Equal(2, summary.TopTemplates[0].InstanceCount); Assert.Equal("$ValveTemplate", summary.TopTemplates[1].TemplateName); Assert.Equal(1, summary.TopTemplates[1].InstanceCount); // Categories ordered by object count descending: UserDefined (3) before Area (1). Assert.Equal(2, summary.ObjectCategories.Count); Assert.Equal(10, summary.ObjectCategories[0].CategoryId); Assert.Equal("UserDefined", summary.ObjectCategories[0].CategoryName); Assert.Equal(3, summary.ObjectCategories[0].ObjectCount); Assert.Equal(13, summary.ObjectCategories[1].CategoryId); Assert.Equal("Area", summary.ObjectCategories[1].CategoryName); Assert.Equal(1, summary.ObjectCategories[1].ObjectCount); } /// /// Verifies that an unknown entry with no objects projects to the /// -equivalent summary. /// [Fact] public void Project_UnknownEmptyEntry_ProducesUnknownEquivalentSummary() { GalaxyHierarchyCacheEntry entry = GalaxyHierarchyCacheEntry.Empty; DashboardGalaxySummary summary = DashboardGalaxySummaryProjector.Project(entry); Assert.Equal(DashboardGalaxyStatus.Unknown, summary.Status); Assert.Null(summary.LastQueriedAt); Assert.Null(summary.LastSuccessAt); Assert.Null(summary.LastDeployTime); Assert.Null(summary.LastError); Assert.Equal(0, summary.ObjectCount); Assert.Equal(0, summary.AreaCount); Assert.Equal(0, summary.AttributeCount); Assert.Equal(0, summary.HistorizedAttributeCount); Assert.Equal(0, summary.AlarmAttributeCount); Assert.Empty(summary.TopTemplates); Assert.Empty(summary.ObjectCategories); } /// /// Verifies that the cache status enum is faithfully mapped to the dashboard /// status enum across every defined value. /// /// The Galaxy cache status to project. /// The expected dashboard status for the given cache status. [Theory] [InlineData(GalaxyCacheStatus.Healthy, DashboardGalaxyStatus.Healthy)] [InlineData(GalaxyCacheStatus.Stale, DashboardGalaxyStatus.Stale)] [InlineData(GalaxyCacheStatus.Unavailable, DashboardGalaxyStatus.Unavailable)] [InlineData(GalaxyCacheStatus.Unknown, DashboardGalaxyStatus.Unknown)] public void Project_MapsCacheStatusToDashboardStatus( GalaxyCacheStatus cacheStatus, DashboardGalaxyStatus expected) { GalaxyHierarchyCacheEntry entry = GalaxyHierarchyCacheEntry.Empty with { Status = cacheStatus }; DashboardGalaxySummary summary = DashboardGalaxySummaryProjector.Project(entry); Assert.Equal(expected, summary.Status); } /// /// Verifies that templates with equal instance counts are ordered /// alphabetically (the ThenBy(name, OrdinalIgnoreCase) tie-break). /// [Fact] public void Project_TopTemplates_TiesBreakAlphabetically() { // Two templates, one instance each, intentionally added B before A so the // result order can only come from the alphabetical tie-break, not insertion. GalaxyObject bObj = MakeObject(1, categoryId: 10, template: "$BTemplate"); GalaxyObject aObj = MakeObject(2, categoryId: 10, template: "$ATemplate"); GalaxyObject[] objects = [bObj, aObj]; GalaxyHierarchyCacheEntry entry = MakeHealthyEntry(objects); DashboardGalaxySummary summary = DashboardGalaxySummaryProjector.Project(entry); Assert.Equal(2, summary.TopTemplates.Count); Assert.Equal("$ATemplate", summary.TopTemplates[0].TemplateName); Assert.Equal(1, summary.TopTemplates[0].InstanceCount); Assert.Equal("$BTemplate", summary.TopTemplates[1].TemplateName); Assert.Equal(1, summary.TopTemplates[1].InstanceCount); } /// /// Verifies that categories with equal object counts are ordered by ascending /// category id (the ThenBy(category.Key) tie-break). /// [Fact] public void Project_ObjectCategories_TiesBreakByAscendingCategoryId() { // One object in category 26 and one in category 10 (equal counts), with the // higher id added first so the result order comes only from the id tie-break. GalaxyObject high = MakeObject(1, categoryId: 26, template: "$T1"); GalaxyObject low = MakeObject(2, categoryId: 10, template: "$T2"); GalaxyObject[] objects = [high, low]; GalaxyHierarchyCacheEntry entry = MakeHealthyEntry(objects); DashboardGalaxySummary summary = DashboardGalaxySummaryProjector.Project(entry); Assert.Equal(2, summary.ObjectCategories.Count); Assert.Equal(10, summary.ObjectCategories[0].CategoryId); Assert.Equal("UserDefined", summary.ObjectCategories[0].CategoryName); Assert.Equal(1, summary.ObjectCategories[0].ObjectCount); Assert.Equal(26, summary.ObjectCategories[1].CategoryId); Assert.Equal("OPCClient", summary.ObjectCategories[1].CategoryName); Assert.Equal(1, summary.ObjectCategories[1].ObjectCount); } /// /// Verifies that is capped at /// the 10 highest-usage templates, and that the dropped template is the correct /// loser by count-then-alphabetical ordering. /// [Fact] public void Project_TopTemplates_CapsAtTenHighestByCount() { // 11 distinct templates. "$Template00" gets the most instances; the rest each // get a unique higher-than-baseline count so the ordering is unambiguous, // except the two lowest ("$KeepZ" and "$DropA") tie at one instance each. // With the count tie, "$DropA" sorts before "$KeepZ" alphabetically, so the // Take(10) cap must KEEP "$DropA" and DROP "$KeepZ" (the alphabetical loser // among the two tied-lowest) — proving cap order is count-then-name, not // insertion. List objects = new(); int nextId = 1; // Nine templates with descending, distinct counts 11..3. for (int i = 0; i < 9; i++) { int count = 11 - i; // 11, 10, ... 3 string template = $"$Template{i:00}"; for (int instance = 0; instance < count; instance++) { objects.Add(MakeObject(nextId++, categoryId: 10, template: template)); } } // Two templates tied at the lowest count (1 each): "$DropA" sorts before // "$KeepZ", so the keeper at slot 10 is "$DropA" and "$KeepZ" is dropped. objects.Add(MakeObject(nextId++, categoryId: 10, template: "$DropA")); objects.Add(MakeObject(nextId++, categoryId: 10, template: "$KeepZ")); GalaxyHierarchyCacheEntry entry = MakeHealthyEntry(objects.ToArray()); DashboardGalaxySummary summary = DashboardGalaxySummaryProjector.Project(entry); Assert.Equal(10, summary.TopTemplates.Count); // The nine distinct-count templates lead, in count order. Assert.Equal("$Template00", summary.TopTemplates[0].TemplateName); Assert.Equal(11, summary.TopTemplates[0].InstanceCount); Assert.Equal("$Template08", summary.TopTemplates[8].TemplateName); Assert.Equal(3, summary.TopTemplates[8].InstanceCount); // Slot 10 is "$DropA" (the alphabetical winner of the lowest-count tie); // "$KeepZ" is the dropped loser. Assert.Equal("$DropA", summary.TopTemplates[9].TemplateName); Assert.Equal(1, summary.TopTemplates[9].InstanceCount); Assert.DoesNotContain(summary.TopTemplates, usage => usage.TemplateName == "$KeepZ"); } /// /// Verifies that rejects a /// null entry with . /// [Fact] public void Project_NullEntry_Throws() { Assert.Throws(() => DashboardGalaxySummaryProjector.Project(null!)); } private static GalaxyObject MakeObject(int gobjectId, int categoryId, string template) { GalaxyObject obj = new() { GobjectId = gobjectId, BrowseName = $"Object{gobjectId}", CategoryId = categoryId, }; obj.TemplateChain.Add(template); return obj; } private static GalaxyHierarchyCacheEntry MakeHealthyEntry(IReadOnlyList objects) { return new GalaxyHierarchyCacheEntry( Status: GalaxyCacheStatus.Healthy, Sequence: 1, LastQueriedAt: null, LastSuccessAt: null, LastDeployTime: null, LastError: null, Objects: objects, Index: GalaxyHierarchyIndex.Build(objects), ObjectCount: objects.Count, AreaCount: 0, AttributeCount: 0, HistorizedAttributeCount: 0, AlarmAttributeCount: 0); } }