using System.Text.Json; using MBN_STOCK_WEBVIEW.LegacyApplication; using MBN_STOCK_WEBVIEW.LegacyBridge; using MMoneyCoderSharp.Data; namespace MBN_STOCK_WEBVIEW.LegacyBridge.Tests; public sealed class LegacyBridgeProtocolTests { [Fact] public void ParseSearch_PreservesBlankEnterAndRejectsExtraAuthority() { Assert.True(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"search-stocks\",\"payload\":{\"text\":\"\",\"trigger\":\"enter\"}}", out var parsed, out var error)); Assert.Null(error); var search = Assert.IsType(parsed); Assert.Equal(string.Empty, search.Text); Assert.Equal(LegacySearchTrigger.Enter, search.Trigger); Assert.False(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"search-stocks\",\"payload\":{\"text\":\"삼성\",\"trigger\":\"button\",\"sceneCode\":\"5001\"}}", out _, out _)); } [Fact] public void ParsePointer_RequiresExactBoundedInput() { const string json = "{\"type\":\"cut-pointer-down\",\"payload\":{\"physicalIndex\":1,\"control\":false,\"shift\":true,\"x\":20,\"y\":45,\"timestampMilliseconds\":1400}}"; Assert.True(LegacyBridgeProtocol.TryParseIntent(json, out var parsed, out _)); var pointer = Assert.IsType(parsed); Assert.Equal(1, pointer.PhysicalIndex); Assert.True(pointer.Shift); Assert.Equal(1_400, pointer.TimestampMilliseconds); } [Theory] [InlineData(49, true)] [InlineData(50, false)] public void ParsePointer_UsesTheFullFiftyRowRuntimeMenuBoundary( int physicalIndex, bool expected) { var json = "{\"type\":\"cut-pointer-down\",\"payload\":{" + $"\"physicalIndex\":{physicalIndex},\"control\":false," + "\"shift\":false,\"x\":0,\"y\":0,\"timestampMilliseconds\":0}}"; Assert.Equal(expected, LegacyBridgeProtocol.TryParseIntent( json, out var parsed, out _)); if (expected) { Assert.Equal( physicalIndex, Assert.IsType(parsed).PhysicalIndex); } } [Theory] [InlineData("arrow-up", LegacyCutKeyboardKey.ArrowUp)] [InlineData("arrow-down", LegacyCutKeyboardKey.ArrowDown)] [InlineData("home", LegacyCutKeyboardKey.Home)] [InlineData("end", LegacyCutKeyboardKey.End)] [InlineData("space", LegacyCutKeyboardKey.Space)] public void ParseCutKeyDown_AcceptsOnlyClosedKeysAndModifiers( string key, LegacyCutKeyboardKey expected) { var json = "{\"type\":\"cut-key-down\",\"payload\":{" + $"\"key\":\"{key}\",\"control\":true,\"shift\":false}}}}"; Assert.True(LegacyBridgeProtocol.TryParseIntent(json, out var parsed, out _)); var intent = Assert.IsType(parsed); Assert.Equal(expected, intent.Key); Assert.True(intent.Control); Assert.False(intent.Shift); } [Theory] [InlineData("page-down", "\"control\":false,\"shift\":false")] [InlineData("ArrowUp", "\"control\":false,\"shift\":false")] [InlineData("home", "\"control\":0,\"shift\":false")] [InlineData("home", "\"control\":false,\"shift\":false,\"physicalIndex\":0")] public void ParseCutKeyDown_RejectsUnknownOrAdditionalAuthority( string key, string fields) { var json = "{\"type\":\"cut-key-down\",\"payload\":{" + $"\"key\":\"{key}\",{fields}}}}}"; Assert.False(LegacyBridgeProtocol.TryParseIntent(json, out _, out _)); } [Fact] public void ParseClearCutSelection_AcceptsOnlyAnEmptyPayload() { Assert.True(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"clear-cut-selection\",\"payload\":{}}", out var parsed, out _)); Assert.IsType(parsed); Assert.False(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"clear-cut-selection\",\"payload\":{\"physicalIndex\":0}}", out _, out _)); } [Fact] public void ParsePlaylistSelection_PreservesModifiersAndRejectsUnsafeAuthority() { Assert.True(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"select-playlist-row\",\"payload\":{\"rowId\":\"legacy-stock-00000003\",\"control\":true,\"shift\":false}}", out var parsed, out _)); var selection = Assert.IsType(parsed); Assert.Equal("legacy-stock-00000003", selection.RowId); Assert.True(selection.Control); Assert.False(selection.Shift); Assert.False(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"select-playlist-row\",\"payload\":{\"rowId\":\"../row\",\"control\":false,\"shift\":false}}", out _, out _)); Assert.False(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"select-playlist-row\",\"payload\":{\"rowId\":\"legacy-stock-00000003\",\"control\":false,\"shift\":false,\"rowIndex\":2}}", out _, out _)); } [Fact] public void ParsePlaylistActivation_AcceptsOnlyOneOpaqueRowId() { Assert.True(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"activate-playlist-row\",\"payload\":{\"rowId\":\"legacy-stock-00000003\"}}", out var parsed, out _)); Assert.Equal( "legacy-stock-00000003", Assert.IsType(parsed).RowId); Assert.False(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"activate-playlist-row\",\"payload\":{\"rowId\":\"../row\"}}", out _, out _)); Assert.False(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"activate-playlist-row\",\"payload\":{\"rowId\":\"legacy-stock-00000003\",\"enabled\":false}}", out _, out _)); } [Fact] public void ParsePlaylistReorder_UsesOnlyOpaqueRowsAndClosedDropPosition() { Assert.True(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"reorder-playlist-rows\",\"payload\":{\"sourceRowId\":\"legacy-stock-00000001\",\"targetRowId\":\"legacy-stock-00000004\",\"position\":\"before\"}}", out var parsed, out _)); var reorder = Assert.IsType(parsed); Assert.Equal("legacy-stock-00000001", reorder.SourceRowId); Assert.Equal("legacy-stock-00000004", reorder.TargetRowId); Assert.Equal(LegacyDragDropPosition.Before, reorder.Position); foreach (var invalid in new[] { "{\"type\":\"reorder-playlist-rows\",\"payload\":{\"sourceRowId\":\"../row\",\"targetRowId\":\"legacy-stock-00000004\",\"position\":\"before\"}}", "{\"type\":\"reorder-playlist-rows\",\"payload\":{\"sourceRowId\":\"legacy-stock-00000001\",\"targetRowId\":\"legacy-stock-00000001\",\"position\":\"before\"}}", "{\"type\":\"reorder-playlist-rows\",\"payload\":{\"sourceRowId\":\"legacy-stock-00000001\",\"targetRowId\":\"legacy-stock-00000004\",\"position\":\"middle\"}}", "{\"type\":\"reorder-playlist-rows\",\"payload\":{\"sourceRowId\":\"legacy-stock-00000001\",\"targetRowId\":\"legacy-stock-00000004\",\"position\":\"before\",\"targetIndex\":3}}" }) { Assert.False(LegacyBridgeProtocol.TryParseIntent(invalid, out _, out _)); } } [Fact] public void ParseAllPlaylistEnabled_RequiresOnlyBooleanState() { Assert.True(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"set-all-playlist-enabled\",\"payload\":{\"enabled\":false}}", out var parsed, out _)); var enabled = Assert.IsType(parsed); Assert.False(enabled.Enabled); Assert.False(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"set-all-playlist-enabled\",\"payload\":{\"enabled\":1}}", out _, out _)); } [Fact] public void ParseTabs_UsesClosedIdentitySet() { Assert.True(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"select-tab\",\"payload\":{\"tabId\":\"kospiIndustry\"}}", out var selection, out _)); Assert.Equal( LegacyOperatorTabId.KospiIndustry, Assert.IsType(selection).TabId); Assert.True(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"swap-tabs\",\"payload\":{\"source\":\"theme\",\"target\":\"exchange\"}}", out var swap, out _)); var parsedSwap = Assert.IsType(swap); Assert.Equal(LegacyOperatorTabId.Theme, parsedSwap.Source); Assert.Equal(LegacyOperatorTabId.Exchange, parsedSwap.Target); Assert.True(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"hover-swap-tabs\",\"payload\":{\"source\":\"theme\",\"target\":\"exchange\",\"expectedSourceIndex\":6,\"expectedTargetIndex\":1}}", out var hoverSwap, out _)); var parsedHover = Assert.IsType(hoverSwap); Assert.Equal(6, parsedHover.ExpectedSourceIndex); Assert.Equal(1, parsedHover.ExpectedTargetIndex); Assert.False(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"hover-swap-tabs\",\"payload\":{\"source\":\"theme\",\"target\":\"exchange\",\"expectedSourceIndex\":6,\"expectedTargetIndex\":10}}", out _, out _)); Assert.False(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"select-tab\",\"payload\":{\"tabId\":\"scene5001\"}}", out _, out _)); } [Theory] [InlineData("first", false)] [InlineData("last", true)] public void ParsePlaylistBoundary_UsesClosedValues(string boundary, bool last) { var json = $"{{\"type\":\"select-playlist-boundary\",\"payload\":{{\"boundary\":\"{boundary}\"}}}}"; Assert.True(LegacyBridgeProtocol.TryParseIntent(json, out var parsed, out _)); Assert.Equal(last, Assert.IsType(parsed).Last); Assert.False(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"select-playlist-boundary\",\"payload\":{\"boundary\":\"middle\"}}", out _, out _)); } [Fact] public void ParseToggleActiveEnabled_RequiresEmptyPayload() { Assert.True(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"toggle-active-playlist-enabled\",\"payload\":{}}", out var parsed, out _)); Assert.IsType(parsed); } [Fact] public void ParseFixedCatalogIntents_DoesNotAcceptSceneAuthority() { Assert.True(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"activate-fixed-action\",\"payload\":{\"actionId\":\"fixed-001\"}}", out var action, out _)); Assert.Equal("fixed-001", Assert.IsType(action).ActionId); Assert.True(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"activate-fixed-section\",\"payload\":{\"sectionIndex\":0}}", out var section, out _)); Assert.Equal(0, Assert.IsType(section).SectionIndex); Assert.False(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"activate-fixed-action\",\"payload\":{\"actionId\":\"fixed-001\",\"sceneCode\":\"5001\"}}", out _, out _)); } [Fact] public void ParseMovingAverages_RequiresBothBooleans() { Assert.True(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"set-moving-averages\",\"payload\":{\"ma5\":true,\"ma20\":false}}", out var parsed, out _)); var intent = Assert.IsType(parsed); Assert.True(intent.Ma5); Assert.False(intent.Ma20); } [Fact] public void ParseIndustryIntents_AcceptsOnlyIndexAndOpaqueActionId() { Assert.True(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"select-industry\",\"payload\":{\"index\":2}}", out var selected, out _)); var selectIntent = Assert.IsType(selected); Assert.Equal(2, selectIntent.Index); Assert.False(selectIntent.DoubleClick); Assert.True(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"double-click-industry\",\"payload\":{\"index\":3}}", out var doubled, out _)); Assert.True(Assert.IsType(doubled).DoubleClick); Assert.True(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"activate-industry-action\",\"payload\":{\"actionId\":\"yield-5일\"}}", out var action, out _)); Assert.Equal( "yield-5일", Assert.IsType(action).ActionId); Assert.True(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"swap-industries\",\"payload\":{}}", out var swap, out _)); Assert.IsType(swap); Assert.False(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"activate-industry-action\",\"payload\":{\"actionId\":\"../5001\"}}", out _, out _)); } [Fact] public void ParseThemeIntents_UsesClosedSessionSortAndOpaqueIds() { Assert.True(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"search-themes\",\"payload\":{\"query\":\"AI\",\"nxtSession\":\"preMarket\"}}", out var search, out _)); Assert.Equal( ThemeSession.PreMarket, Assert.IsType(search).NxtSession); Assert.True(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"select-theme\",\"payload\":{\"resultId\":\"theme-result-001\"}}", out var selection, out _)); Assert.Equal( "theme-result-001", Assert.IsType(selection).ResultId); Assert.True(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"set-theme-sort\",\"payload\":{\"sortId\":\"GAIN_DESC\"}}", out var sort, out _)); Assert.Equal("GAIN_DESC", Assert.IsType(sort).SortId); Assert.True(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"activate-theme-action\",\"payload\":{\"actionId\":\"five-row-current\"}}", out var action, out _)); Assert.Equal( "five-row-current", Assert.IsType(action).ActionId); Assert.False(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"search-themes\",\"payload\":{\"query\":\"AI\",\"nxtSession\":\"auto\"}}", out _, out _)); } [Fact] public void ParseExpertAndTradingHaltIntents_AreClosed() { Assert.True(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"search-experts\",\"payload\":{\"query\":\"김\"}}", out var expertSearch, out _)); Assert.Equal("김", Assert.IsType(expertSearch).Query); Assert.True(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"select-expert\",\"payload\":{\"selectionId\":\"expert-result-0001-0001\"}}", out var expertSelect, out _)); Assert.IsType(expertSelect); Assert.True(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"activate-expert-action\",\"payload\":{\"actionId\":\"five-row-current\"}}", out var expertAction, out _)); Assert.IsType(expertAction); Assert.True(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"search-trading-halts\",\"payload\":{\"query\":\"삼성\"}}", out var haltSearch, out _)); Assert.IsType(haltSearch); Assert.True(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"select-trading-halt\",\"payload\":{\"selectionId\":\"trading-halt-result-0001-0001\"}}", out var haltSelect, out _)); Assert.IsType(haltSelect); Assert.True(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"toggle-trading-halt-sort\",\"payload\":{}}", out var haltSort, out _)); Assert.IsType(haltSort); Assert.True(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"activate-trading-halt-action\",\"payload\":{\"actionId\":\"candle-120d\"}}", out var haltAction, out _)); Assert.IsType(haltAction); } [Theory] [InlineData("up", LegacyPlaylistMoveDirection.Up)] [InlineData("down", LegacyPlaylistMoveDirection.Down)] public void ParsePlaylistMove_UsesStrictDirection( string direction, LegacyPlaylistMoveDirection expected) { var json = $"{{\"type\":\"move-selected-playlist-rows\",\"payload\":{{\"direction\":\"{direction}\"}}}}"; Assert.True(LegacyBridgeProtocol.TryParseIntent(json, out var parsed, out _)); var move = Assert.IsType(parsed); Assert.Equal(expected, move.Direction); Assert.False(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"move-selected-playlist-rows\",\"payload\":{\"direction\":\"first\"}}", out _, out _)); } [Theory] [InlineData("delete-selected-playlist-rows", typeof(LegacyDeleteSelectedPlaylistRowsIntent))] [InlineData("delete-all-playlist-rows", typeof(LegacyDeleteAllPlaylistRowsIntent))] public void ParsePlaylistDelete_RequiresEmptyPayload(string type, Type expectedType) { Assert.True(LegacyBridgeProtocol.TryParseIntent( $"{{\"type\":\"{type}\",\"payload\":{{}}}}", out var parsed, out _)); Assert.IsType(expectedType, parsed); Assert.False(LegacyBridgeProtocol.TryParseIntent( $"{{\"type\":\"{type}\",\"payload\":{{\"rowId\":\"legacy-stock-00000001\"}}}}", out _, out _)); } [Fact] public void ParseNamedPlaylistIntents_AcceptsOnlyOpaqueIdsTitlesAndDispatchIds() { Assert.True(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"refresh-named-playlists\",\"payload\":{\"requestId\":\"named-ui-1\"}}", out var refresh, out _)); Assert.Equal( "named-ui-1", Assert.IsType(refresh).RequestId); Assert.True(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"select-named-playlist\",\"payload\":{\"definitionId\":\"named-definition-AAAAAAAAAAAB\",\"requestId\":\"named-ui-2\"}}", out var selection, out _)); var selectionIntent = Assert.IsType(selection); Assert.Equal("named-definition-AAAAAAAAAAAB", selectionIntent.DefinitionId); Assert.Equal("named-ui-2", selectionIntent.RequestId); Assert.True(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"create-named-playlist\",\"payload\":{\"title\":\"아침 방송 A^B\",\"requestId\":\"named-ui-2\"}}", out var create, out _)); var createIntent = Assert.IsType(create); Assert.Equal("아침 방송 A^B", createIntent.Title); Assert.Equal("named-ui-2", createIntent.RequestId); Assert.True(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"delete-selected-named-playlist\",\"payload\":{\"requestId\":\"named-ui-3\"}}", out var delete, out _)); Assert.Equal( "named-ui-3", Assert.IsType(delete).RequestId); foreach (var type in new[] { "load-selected-named-playlist", "save-current-named-playlist" }) { Assert.True(LegacyBridgeProtocol.TryParseIntent( $"{{\"type\":\"{type}\",\"payload\":{{}}}}", out var emptyIntent, out _)); Assert.NotNull(emptyIntent); } Assert.False(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"select-named-playlist\",\"payload\":{\"definitionId\":\"named-definition-AAAAAAAAAAAB\",\"programCode\":\"00000001\"}}", out _, out _)); Assert.False(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"select-named-playlist\",\"payload\":{\"definitionId\":\"named-definition-AAAAAAAAAAAB\"}}", out _, out _)); Assert.False(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"save-current-named-playlist\",\"payload\":{\"rows\":[]}}", out _, out _)); Assert.False(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"create-named-playlist\",\"payload\":{\"title\":\" padded \",\"requestId\":\"named-ui-4\"}}", out _, out _)); Assert.False(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"refresh-named-playlists\",\"payload\":{}}", out _, out _)); Assert.False(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"delete-selected-named-playlist\",\"payload\":{}}", out _, out _)); } [Theory] [InlineData("https://legacy-parity.mbn.local/index.html", true)] [InlineData("https://legacy-parity.mbn.local.evil/index.html", false)] [InlineData("http://legacy-parity.mbn.local/index.html", false)] [InlineData("file:///C:/index.html", false)] public void TrustedSource_IsExactHttpsOrigin(string source, bool expected) { Assert.Equal(expected, LegacyBridgeProtocol.IsTrustedSource(source)); } [Fact] public void SerializedState_DoesNotExposeHiddenDbOrSceneAuthority() { var playlistRow = LegacyStockCutCatalog.CreatePlaylistRow( "legacy-stock-00000001", "삼성전자", new LegacyStockIdentity(LegacyDomesticStockMarket.Kospi, "삼성전자", "005930"), LegacyStockCutCatalog.Rows[1]); var snapshot = new LegacyOperatorSnapshot( 1, "삼성", [new LegacyStockResultRow(0, "삼성전자")], 0, [new LegacyCutViewRow(1, 2, "1열판기본_현재가", false, true, true)], [playlistRow], null, string.Empty, LegacyOperatorStatusKind.Neutral); var json = LegacyBridgeProtocol.SerializeState(snapshot, isBusy: true); Assert.DoesNotContain("005930", json, StringComparison.Ordinal); Assert.DoesNotContain("5001", json, StringComparison.Ordinal); Assert.DoesNotContain("rawCutLabel", json, StringComparison.Ordinal); using var document = JsonDocument.Parse(json); var payload = document.RootElement.GetProperty("payload"); Assert.Equal( "삼성전자", payload.GetProperty("searchResults")[0].GetProperty("displayName").GetString()); Assert.Equal( "삼성전자", payload.GetProperty("playlist")[0].GetProperty("stockName").GetString()); Assert.Equal( "1열판기본_현재가", payload.GetProperty("cutRows")[0].GetProperty("rawLabel").GetString()); Assert.True(payload.GetProperty("cutRows")[0].GetProperty("isFocused").GetBoolean()); Assert.False(payload.GetProperty("playlist")[0].GetProperty("isSelected").GetBoolean()); Assert.False(payload.GetProperty("playlist")[0].GetProperty("isActive").GetBoolean()); Assert.True(payload.GetProperty("isBusy").GetBoolean()); } [Fact] public void SerializedState_PreservesInjectedRuntimeCutMenuOrderAndSeparators() { var controller = new LegacyOperatorController( new EmptyStockLookup(), cutRows: [ new LegacyCutCatalogRow( 0, 1, LegacyStockCutCatalog.Rows[1].RawLabel, IsSeparator: false), new LegacyCutCatalogRow(1, null, string.Empty, IsSeparator: true), new LegacyCutCatalogRow( 2, 2, LegacyStockCutCatalog.Rows[0].RawLabel, IsSeparator: false) ]); var json = LegacyBridgeProtocol.SerializeState(controller.Current); using var document = JsonDocument.Parse(json); var rows = document.RootElement.GetProperty("payload").GetProperty("cutRows"); Assert.Equal(3, rows.GetArrayLength()); Assert.Equal("1열판기본_현재가", rows[0].GetProperty("rawLabel").GetString()); Assert.Equal(0, rows[0].GetProperty("physicalIndex").GetInt32()); Assert.True(rows[1].GetProperty("isSeparator").GetBoolean()); Assert.Equal(string.Empty, rows[1].GetProperty("rawLabel").GetString()); Assert.Equal("1열판기본_예상체결가", rows[2].GetProperty("rawLabel").GetString()); Assert.Equal(2, rows[2].GetProperty("displayOrdinal").GetInt32()); } [Fact] public async Task SerializedNamedPlaylist_ProjectsOnlyOpaqueListAndSafetyState() { var workflow = new LegacyNamedPlaylistWorkflowController( new BridgeNamedPlaylistPersistence()); await workflow.RefreshAsync(); workflow.SelectDefinition(workflow.Current.Definitions.Single().DefinitionId); await workflow.LoadSelectedAsync(); var snapshot = new LegacyOperatorSnapshot( 1, string.Empty, [], null, [], [], null, string.Empty, LegacyOperatorStatusKind.Neutral, NamedPlaylist: workflow.Current); var json = LegacyBridgeProtocol.SerializeState(snapshot); Assert.DoesNotContain("87654321", json, StringComparison.Ordinal); Assert.DoesNotContain("SECRET-SUBJECT", json, StringComparison.Ordinal); Assert.DoesNotContain("SECRET-DATA", json, StringComparison.Ordinal); Assert.DoesNotContain("programCode", json, StringComparison.OrdinalIgnoreCase); Assert.DoesNotContain("listText", json, StringComparison.OrdinalIgnoreCase); using var document = JsonDocument.Parse(json); var named = document.RootElement.GetProperty("payload").GetProperty("namedPlaylist"); Assert.Equal("저녁 방송", named.GetProperty("definitions")[0] .GetProperty("title").GetString()); Assert.Equal(1, named.GetProperty("rowCount").GetInt32()); Assert.False(named.TryGetProperty("rows", out _)); Assert.True(named.GetProperty("canLoad").GetBoolean()); } private sealed class BridgeNamedPlaylistPersistence : INamedPlaylistPersistenceService { private static readonly NamedPlaylistSummary Summary = new("87654321", "저녁 방송"); public bool CanMutate => true; public Task ListAsync( int maximumResults = LegacyNamedPlaylistPersistenceService.DefaultMaximumDefinitions, CancellationToken cancellationToken = default) => Task.FromResult(new NamedPlaylistListResult( DateTimeOffset.UtcNow, [Summary], false)); public Task SuggestNextProgramCodeAsync( CancellationToken cancellationToken = default) => Task.FromResult("87654322"); public Task LoadAsync( string programCode, CancellationToken cancellationToken = default) => Task.FromResult(new NamedPlaylistDocument( Summary, DateTimeOffset.UtcNow, [new NamedPlaylistStoredItem( 0, true, new MBN_STOCK_WEBVIEW.Core.Playout.Scenes.LegacySceneSelection( "SECRET-GROUP", "SECRET-SUBJECT", "SECRET-GRAPHIC", "SECRET-SUBTYPE", "SECRET-DATA"), new NamedPlaylistPageState(1, 1))])); public Task CreateDefinitionAsync( string programCode, string title, CancellationToken cancellationToken = default) => Task.CompletedTask; public Task ReplaceItemsAsync( string programCode, IReadOnlyList items, CancellationToken cancellationToken = default) => Task.CompletedTask; public Task DeleteAsync( string programCode, CancellationToken cancellationToken = default) => Task.CompletedTask; } private sealed class EmptyStockLookup : ILegacyStockLookup { public Task SearchAsync( string query, CancellationToken cancellationToken = default) => Task.FromResult(new LegacyStockSearchData(query, [], [])); } }