using System.Collections; using System.Text; using System.Text.Json.Nodes; using MBN_STOCK_WEBVIEW.Core.Playout.Scenes; namespace MBN_STOCK_WEBVIEW.LegacyApplication.Tests; public sealed class LegacyFixedActionCatalogTests { [Fact] public void EmbeddedCatalog_HasApprovedIdentityCountsAndOrder() { var catalog = LegacyFixedActionCatalog.Default; Assert.Equal(LegacyFixedActionCatalog.ExpectedDocumentSha256, catalog.DocumentSha256); Assert.Equal( LegacyFixedActionCatalog.ExpectedGeneratorSourceSha256, catalog.GeneratorSourceSha256); Assert.Equal(328, catalog.Actions.Count); Assert.Equal(Enumerable.Range(1, 328).Select(index => $"fixed-{index:000}"), catalog.Actions.Select(action => action.Id)); Assert.Equal(78, catalog.GetActions(LegacyFixedMarket.Overseas).Count); Assert.Equal(46, catalog.GetActions(LegacyFixedMarket.Exchange).Count); Assert.Equal(204, catalog.GetActions(LegacyFixedMarket.Index).Count); Assert.Equal(322, catalog.Actions.Count(action => action.Available)); Assert.Equal(6, catalog.Actions.Count(action => !action.Available)); } [Fact] public void EmbeddedCatalog_ClosesEverySceneIdentityAgainstCoreCatalog() { foreach (var action in LegacyFixedActionCatalog.Default.Actions) { var definition = Assert.Single( LegacySceneCatalog.Definitions, candidate => candidate.BuilderKey == action.BuilderKey); Assert.Single(action.Aliases); Assert.Equal(action.Code, action.Aliases[0]); Assert.Contains(action.Code, definition.CutAliases); Assert.Contains( LegacySceneCatalog.FindByCutAlias(action.Code), candidate => candidate.BuilderKey == action.BuilderKey); } } [Fact] public void RuntimeAssets_AreApprovedAndImmutable() { var catalog = LegacyFixedActionCatalog.Default; Assert.Equal( "bin/Debug/Res/해외.ini", catalog.GetRuntimeAsset(LegacyFixedMarket.Overseas).RelativePath); Assert.Equal( "DBDC4EAC28BED9FD0DEABED7FE880F21CB49E48503FD3CCB95C0A29128818B2A", catalog.GetRuntimeAsset(LegacyFixedMarket.Overseas).Sha256); Assert.Equal( "bin/Debug/Res/환율.ini", catalog.GetRuntimeAsset(LegacyFixedMarket.Exchange).RelativePath); Assert.Equal( "bin/Debug/Res/지수.ini", catalog.GetRuntimeAsset(LegacyFixedMarket.Index).RelativePath); var dictionary = Assert.IsAssignableFrom(catalog.RuntimeAssets); Assert.True(dictionary.IsReadOnly); Assert.Throws(() => dictionary.Add( LegacyFixedMarket.Overseas, new LegacyFixedRuntimeAsset(LegacyFixedMarket.Overseas, "bad", "bad"))); } [Fact] public void QueryApi_ReturnsImmutableMarketAndSectionViews() { var catalog = LegacyFixedActionCatalog.Default; var overseas = catalog.GetActions(LegacyFixedMarket.Overseas); var sections = catalog.GetSections(LegacyFixedMarket.Overseas); var firstSection = sections[0]; Assert.Equal(LegacyFixedMarket.Overseas, firstSection.Market); Assert.Equal("1열판기본", firstSection.Name); Assert.Equal(43, firstSection.Actions.Count); Assert.Same(catalog.Actions[0], catalog.GetAction("fixed-001")); Assert.Same( firstSection.Actions[0], catalog.GetActions(LegacyFixedMarket.Overseas, "1열판기본")[0]); Assert.Empty(catalog.GetActions(LegacyFixedMarket.Overseas, "없는 섹션")); var mutableActions = Assert.IsAssignableFrom>(overseas); Assert.True(mutableActions.IsReadOnly); Assert.Throws(() => mutableActions.RemoveAt(0)); Assert.Null(catalog.FindAction(null)); Assert.Null(catalog.FindAction("fixed-999")); Assert.Throws(() => catalog.GetAction("fixed-999")); Assert.Throws(() => catalog.GetActions((LegacyFixedMarket)99)); } [Fact] public void Materialize_ComputesDateFromTrustedHostClock() { var now = new DateTimeOffset(2026, 7, 15, 23, 58, 0, TimeSpan.FromHours(9)); var result = LegacyFixedActionCatalog.Default.Materialize("fixed-248", now); Assert.Equal("2026-07-15", result.Selection.DataCode); Assert.Equal("", result.Selection.GraphicType); Assert.Equal("$TODAY", result.Action.Selection.DataCode); Assert.Equal(LegacyFixedMarket.Index, result.RuntimeAsset.Market); } [Theory] [InlineData(0, "PRE_MARKET")] [InlineData(12, "PRE_MARKET")] [InlineData(13, "AFTER_MARKET")] [InlineData(23, "AFTER_MARKET")] public void Materialize_ComputesNxtSessionFromTrustedHostClock(int hour, string expected) { var now = new DateTimeOffset(2026, 7, 15, hour, 59, 0, TimeSpan.FromHours(9)); var result = LegacyFixedActionCatalog.Default.Materialize("fixed-263", now); Assert.Equal(expected, result.Selection.GraphicType); Assert.Equal("$NXT_SESSION", result.Action.Selection.GraphicType); Assert.Equal("", result.Selection.DataCode); } [Fact] public void Materialize_LeavesMovingAverageStateOutOfTheActionIdentity() { var result = LegacyFixedActionCatalog.Default.Materialize( "fixed-136", new DateTimeOffset(2026, 7, 15, 9, 0, 0, TimeSpan.FromHours(9))); Assert.Equal(string.Empty, result.Selection.GraphicType); Assert.Equal("", result.Action.Selection.GraphicType); } [Fact] public void Materialize_RetainsGraphicTypeForOtherBuilders() { var result = LegacyFixedActionCatalog.Default.Materialize( "fixed-001", DateTimeOffset.Now); Assert.Equal("1열판기본", result.Selection.GraphicType); } [Fact] public void Materialize_ResolvesEveryTemplateWithoutAcceptingSelectionOverrides() { var catalog = LegacyFixedActionCatalog.Default; var beforeBoundary = new DateTimeOffset( 2026, 7, 15, 12, 59, 0, TimeSpan.FromHours(9)); var afterBoundary = beforeBoundary.AddHours(1); foreach (var action in catalog.Actions.Where(action => action.Available)) { var before = catalog.Materialize( action.Id, beforeBoundary); Assert.DoesNotContain('$', before.Selection.GroupCode); Assert.DoesNotContain('$', before.Selection.Subject); Assert.DoesNotContain('$', before.Selection.GraphicType); Assert.DoesNotContain('$', before.Selection.Subtype); Assert.DoesNotContain('$', before.Selection.DataCode); if (action.DynamicDate) { Assert.Equal("2026-07-15", before.Selection.DataCode); } else if (action.DynamicNxtSession) { Assert.Equal("PRE_MARKET", before.Selection.GraphicType); Assert.Equal( "AFTER_MARKET", catalog.Materialize(action.Id, afterBoundary).Selection.GraphicType); } else if (action.SupportsMovingAverages) { Assert.Equal(action.Selection.GraphicType, before.Selection.GraphicType); } else { Assert.Equal(action.Selection, before.Selection); } } Assert.Equal(50, catalog.Actions.Count(action => action.DynamicDate)); Assert.Equal(28, catalog.Actions.Count(action => action.DynamicNxtSession)); Assert.Equal(74, catalog.Actions.Count(action => action.SupportsMovingAverages)); } [Fact] public void Materialize_RejectsUnknownAndUnavailableActions() { var catalog = LegacyFixedActionCatalog.Default; Assert.Throws(() => catalog.Materialize("fixed-999", DateTimeOffset.Now)); var exception = Assert.Throws(() => catalog.Materialize("fixed-245", DateTimeOffset.Now)); Assert.Contains("수동 순매도", exception.Message, StringComparison.Ordinal); } [Fact] public void Loader_RejectsUnknownJsonMembersAndMissingRequiredMembers() { var unknown = ReadDocument(); unknown["unexpected"] = true; AssertInvalid(unknown, "JSON schema"); var nestedUnknown = ReadDocument(); Action(nestedUnknown, 0)["selection"]!["webOverride"] = "5006"; AssertInvalid(nestedUnknown, "JSON schema"); var missing = ReadDocument(); missing.AsObject().Remove("schemaVersion"); AssertInvalid(missing, "JSON schema"); var nullActions = ReadDocument(); nullActions["actions"] = null; AssertInvalid(nullActions, "actions is required"); } [Fact] public void Loader_RejectsDuplicateJsonMembers() { var original = Encoding.UTF8.GetString(ReadEmbeddedBytes()); var duplicate = original.Replace( "\"schemaVersion\": 1,", "\"schemaVersion\": 1,\n \"schemaVersion\": 1,", StringComparison.Ordinal); var exception = Assert.Throws(() => LegacyFixedActionCatalog.LoadForValidation(Encoding.UTF8.GetBytes(duplicate))); Assert.Contains("JSON schema", exception.Message, StringComparison.Ordinal); Assert.IsType(exception.InnerException); } [Fact] public void Loader_RejectsTamperedDocumentHash() { var tampered = ReadEmbeddedBytes().Concat([(byte)' ']).ToArray(); var exception = Assert.Throws(() => LegacyFixedActionCatalog.LoadForValidation( tampered, LegacyFixedActionCatalog.ExpectedDocumentSha256)); Assert.Contains("hash is invalid", exception.Message, StringComparison.Ordinal); } [Fact] public void Loader_RejectsUnknownBuilderCodeAndAlias() { var unknownBuilder = ReadDocument(); Action(unknownBuilder, 0)["builderKey"] = "s9999"; AssertInvalid(unknownBuilder, "unknown builder"); var wrongCode = ReadDocument(); Action(wrongCode, 0)["code"] = "5006"; AssertInvalid(wrongCode, "is not an alias"); var wrongAlias = ReadDocument(); Action(wrongAlias, 0)["aliases"] = new JsonArray("N5001"); AssertInvalid(wrongAlias, "sole alias"); } [Fact] public void Loader_RejectsNonSequentialIdAndSelectionPlaceholders() { var wrongId = ReadDocument(); Action(wrongId, 0)["id"] = "fixed-002"; AssertInvalid(wrongId, "action id at index 0"); var untrustedSelection = ReadDocument(); Action(untrustedSelection, 0)["selection"]!["graphicType"] = "$WEB_VALUE"; AssertInvalid(untrustedSelection, "unapproved selection placeholder"); var badDynamicDate = ReadDocument(); Action(badDynamicDate, 247)["dynamicDate"] = false; AssertInvalid(badDynamicDate, "dynamic-date declaration"); } [Fact] public void Loader_RejectsTamperedRuntimeAssetMetadata() { var path = ReadDocument(); path["runtimeAssets"]!["overseas"]!["relativePath"] = "C:/untrusted/해외.ini"; AssertInvalid(path, "runtime asset metadata"); var hash = ReadDocument(); hash["runtimeAssets"]!["index"]!["sha256"] = new string('0', 64); AssertInvalid(hash, "runtime asset metadata"); } private static JsonNode ReadDocument() => JsonNode.Parse(ReadEmbeddedBytes()) ?? throw new InvalidOperationException(); private static byte[] ReadEmbeddedBytes() { using var stream = typeof(LegacyFixedActionCatalog).Assembly.GetManifestResourceStream( "MBN_STOCK_WEBVIEW.LegacyApplication.Catalog.LegacyFixedActions.json") ?? throw new InvalidOperationException("Embedded test catalog was not found."); using var buffer = new MemoryStream(); stream.CopyTo(buffer); return buffer.ToArray(); } private static JsonObject Action(JsonNode document, int index) => document["actions"]!.AsArray()[index]!.AsObject(); private static void AssertInvalid(JsonNode document, string expectedMessage) { var bytes = Encoding.UTF8.GetBytes(document.ToJsonString()); var exception = Assert.Throws(() => LegacyFixedActionCatalog.LoadForValidation(bytes)); Assert.Contains(expectedMessage, exception.Message, StringComparison.OrdinalIgnoreCase); } }