Files
MBN_STOCK_WEBVIEW/tests/MBN_STOCK_WEBVIEW.LegacyBridge.Tests/LegacyBridgeProtocolTests.cs

785 lines
33 KiB
C#

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<LegacySearchStocksIntent>(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,\"allowAppend\":false}}";
Assert.True(LegacyBridgeProtocol.TryParseIntent(json, out var parsed, out _));
var pointer = Assert.IsType<LegacyCutPointerDownIntent>(parsed);
Assert.Equal(1, pointer.PhysicalIndex);
Assert.True(pointer.Shift);
Assert.Equal(1_400, pointer.TimestampMilliseconds);
Assert.False(pointer.AllowAppend);
Assert.False(LegacyBridgeProtocol.TryParseIntent(
"{\"type\":\"cut-pointer-down\",\"payload\":{\"physicalIndex\":1,\"control\":false,\"shift\":true,\"x\":20,\"y\":45,\"timestampMilliseconds\":1400}}",
out _,
out _));
Assert.False(LegacyBridgeProtocol.TryParseIntent(
"{\"type\":\"cut-pointer-down\",\"payload\":{\"physicalIndex\":1,\"control\":false,\"shift\":true,\"x\":20,\"y\":45,\"timestampMilliseconds\":1400,\"allowAppend\":0}}",
out _,
out _));
Assert.False(LegacyBridgeProtocol.TryParseIntent(
"{\"type\":\"cut-pointer-down\",\"payload\":{\"physicalIndex\":1,\"control\":false,\"shift\":true,\"x\":20,\"y\":45,\"timestampMilliseconds\":1400,\"allowAppend\":true,\"sceneCode\":\"5001\"}}",
out _,
out _));
}
[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," +
"\"allowAppend\":true}}";
Assert.Equal(expected, LegacyBridgeProtocol.TryParseIntent(
json,
out var parsed,
out _));
if (expected)
{
Assert.Equal(
physicalIndex,
Assert.IsType<LegacyCutPointerDownIntent>(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<LegacyCutKeyDownIntent>(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<LegacyClearCutSelectionIntent>(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<LegacySelectPlaylistRowIntent>(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<LegacyActivatePlaylistRowIntent>(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<LegacyReorderPlaylistRowsIntent>(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<LegacySetAllPlaylistEnabledIntent>(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<LegacySelectTabIntent>(selection).TabId);
Assert.True(LegacyBridgeProtocol.TryParseIntent(
"{\"type\":\"swap-tabs\",\"payload\":{\"source\":\"theme\",\"target\":\"exchange\"}}",
out var swap,
out _));
var parsedSwap = Assert.IsType<LegacySwapTabsIntent>(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<LegacyHoverSwapTabsIntent>(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<LegacySelectPlaylistBoundaryIntent>(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<LegacyToggleActivePlaylistEnabledIntent>(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<LegacyActivateFixedActionIntent>(action).ActionId);
Assert.True(LegacyBridgeProtocol.TryParseIntent(
"{\"type\":\"activate-fixed-section\",\"payload\":{\"sectionIndex\":0}}",
out var section,
out _));
Assert.Equal(0, Assert.IsType<LegacyActivateFixedSectionIntent>(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<LegacySetMovingAveragesIntent>(parsed);
Assert.True(intent.Ma5);
Assert.False(intent.Ma20);
}
[Fact]
public void ParseIndustryIntents_AcceptsOnlyClosedSelectionTextAndOpaqueActionId()
{
Assert.True(LegacyBridgeProtocol.TryParseIntent(
"{\"type\":\"select-industry\",\"payload\":{\"index\":2}}",
out var selected,
out _));
var selectIntent = Assert.IsType<LegacySelectIndustryIntent>(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<LegacySelectIndustryIntent>(doubled).DoubleClick);
Assert.True(LegacyBridgeProtocol.TryParseIntent(
"{\"type\":\"activate-industry-action\",\"payload\":{\"actionId\":\"yield-5일\"}}",
out var action,
out _));
Assert.Equal(
"yield-5일",
Assert.IsType<LegacyActivateIndustryActionIntent>(action).ActionId);
Assert.True(LegacyBridgeProtocol.TryParseIntent(
"{\"type\":\"swap-industries\",\"payload\":{}}",
out var swap,
out _));
Assert.IsType<LegacySwapIndustriesIntent>(swap);
Assert.True(LegacyBridgeProtocol.TryParseIntent(
"{\"type\":\"set-industry-comparison-text\",\"payload\":{\"slot\":\"first\",\"value\":\"직접 입력_%\"}}",
out var edit,
out _));
var editIntent = Assert.IsType<LegacySetIndustryComparisonTextIntent>(edit);
Assert.Equal(LegacyIndustryComparisonSlot.First, editIntent.Slot);
Assert.Equal("직접 입력_%", editIntent.Value);
Assert.True(LegacyBridgeProtocol.TryParseIntent(
"{\"type\":\"set-industry-comparison-text\",\"payload\":{\"slot\":\"second\",\"value\":\"\"}}",
out var clear,
out _));
Assert.Equal(string.Empty,
Assert.IsType<LegacySetIndustryComparisonTextIntent>(clear).Value);
Assert.False(LegacyBridgeProtocol.TryParseIntent(
"{\"type\":\"activate-industry-action\",\"payload\":{\"actionId\":\"../5001\"}}",
out _,
out _));
Assert.False(LegacyBridgeProtocol.TryParseIntent(
"{\"type\":\"set-industry-comparison-text\",\"payload\":{\"slot\":\"third\",\"value\":\"업종\"}}",
out _,
out _));
Assert.False(LegacyBridgeProtocol.TryParseIntent(
"{\"type\":\"set-industry-comparison-text\",\"payload\":{\"slot\":\"first\",\"value\":\"업종\\n이름\"}}",
out _,
out _));
Assert.False(LegacyBridgeProtocol.TryParseIntent(
"{\"type\":\"set-industry-comparison-text\",\"payload\":{\"slot\":\"first\",\"value\":\"업종\",\"actionId\":\"two-column\"}}",
out _,
out _));
var oversized = new string('가',
LegacyIndustrySelectionWorkflow.MaximumComparisonTextLength + 1);
var oversizedJson = "{\"type\":\"set-industry-comparison-text\",\"payload\":{\"slot\":\"first\",\"value\":" +
System.Text.Json.JsonSerializer.Serialize(oversized) + "}}";
Assert.False(LegacyBridgeProtocol.TryParseIntent(oversizedJson, out _, out _));
}
[Fact]
public void ParseThemeIntents_UsesQueryOnlySearchSortAndOpaqueIds()
{
Assert.True(LegacyBridgeProtocol.TryParseIntent(
"{\"type\":\"search-themes\",\"payload\":{\"query\":\"AI\"}}",
out var search,
out _));
Assert.Equal("AI", Assert.IsType<LegacySearchThemesIntent>(search).Query);
Assert.True(LegacyBridgeProtocol.TryParseIntent(
"{\"type\":\"select-theme\",\"payload\":{\"resultId\":\"theme-result-001\"}}",
out var selection,
out _));
Assert.Equal(
"theme-result-001",
Assert.IsType<LegacySelectThemeIntent>(selection).ResultId);
Assert.True(LegacyBridgeProtocol.TryParseIntent(
"{\"type\":\"set-theme-sort\",\"payload\":{\"sortId\":\"GAIN_DESC\"}}",
out var sort,
out _));
Assert.Equal("GAIN_DESC", Assert.IsType<LegacySetThemeSortIntent>(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<LegacyActivateThemeActionIntent>(action).ActionId);
Assert.False(LegacyBridgeProtocol.TryParseIntent(
"{\"type\":\"search-themes\",\"payload\":{\"query\":\"AI\",\"nxtSession\":\"preMarket\"}}",
out _,
out _));
}
[Fact]
public void ParseExpertAndTradingHaltIntents_AreClosed()
{
Assert.True(LegacyBridgeProtocol.TryParseIntent(
"{\"type\":\"search-experts\",\"payload\":{\"query\":\"김\"}}",
out var expertSearch,
out _));
Assert.Equal("김", Assert.IsType<LegacySearchExpertsIntent>(expertSearch).Query);
Assert.True(LegacyBridgeProtocol.TryParseIntent(
"{\"type\":\"select-expert\",\"payload\":{\"selectionId\":\"expert-result-0001-0001\"}}",
out var expertSelect,
out _));
Assert.IsType<LegacySelectExpertIntent>(expertSelect);
Assert.True(LegacyBridgeProtocol.TryParseIntent(
"{\"type\":\"activate-expert-action\",\"payload\":{\"actionId\":\"five-row-current\"}}",
out var expertAction,
out _));
Assert.IsType<LegacyActivateExpertActionIntent>(expertAction);
Assert.True(LegacyBridgeProtocol.TryParseIntent(
"{\"type\":\"search-trading-halts\",\"payload\":{\"query\":\"삼성\"}}",
out var haltSearch,
out _));
Assert.IsType<LegacySearchTradingHaltsIntent>(haltSearch);
Assert.True(LegacyBridgeProtocol.TryParseIntent(
"{\"type\":\"select-trading-halt\",\"payload\":{\"selectionId\":\"trading-halt-result-0001-0001\"}}",
out var haltSelect,
out _));
Assert.IsType<LegacySelectTradingHaltIntent>(haltSelect);
Assert.True(LegacyBridgeProtocol.TryParseIntent(
"{\"type\":\"toggle-trading-halt-sort\",\"payload\":{}}",
out var haltSort,
out _));
Assert.IsType<LegacyToggleTradingHaltSortIntent>(haltSort);
Assert.True(LegacyBridgeProtocol.TryParseIntent(
"{\"type\":\"activate-trading-halt-action\",\"payload\":{\"actionId\":\"candle-120d\"}}",
out var haltAction,
out _));
Assert.IsType<LegacyActivateTradingHaltActionIntent>(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<LegacyMoveSelectedPlaylistRowsIntent>(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<LegacyRefreshNamedPlaylistsIntent>(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<LegacySelectNamedPlaylistIntent>(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<LegacyCreateNamedPlaylistIntent>(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<LegacyDeleteSelectedNamedPlaylistIntent>(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 SerializedIndustryStateProjectsAuthoritativeEditedTextNotTypedIdentityName()
{
var identity = new IndustrySelection(IndustryMarket.Kospi, "DB 업종명", "013");
var industry = new LegacyIndustrySelectionSnapshot(
3,
IndustryMarket.Kospi,
[identity],
LegacyIndustryActionCatalog.GetActions(IndustryMarket.Kospi),
0,
identity,
identity,
null,
"직접 입력 첫째",
"직접 입력 둘째");
var snapshot = new LegacyOperatorSnapshot(
1,
string.Empty,
[],
null,
[],
[],
null,
string.Empty,
LegacyOperatorStatusKind.Neutral,
Industry: industry);
using var document = JsonDocument.Parse(LegacyBridgeProtocol.SerializeState(snapshot));
var projected = document.RootElement.GetProperty("payload").GetProperty("industry");
Assert.Equal("직접 입력 첫째", projected.GetProperty("firstName").GetString());
Assert.Equal("직접 입력 둘째", projected.GetProperty("secondName").GetString());
Assert.NotEqual("DB 업종명", projected.GetProperty("firstName").GetString());
}
[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<NamedPlaylistListResult> ListAsync(
int maximumResults = LegacyNamedPlaylistPersistenceService.DefaultMaximumDefinitions,
CancellationToken cancellationToken = default) =>
Task.FromResult(new NamedPlaylistListResult(
DateTimeOffset.UtcNow,
[Summary],
false));
public Task<string> SuggestNextProgramCodeAsync(
CancellationToken cancellationToken = default) =>
Task.FromResult("87654322");
public Task<NamedPlaylistDocument> 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<NamedPlaylistStoredItem> items,
CancellationToken cancellationToken = default) => Task.CompletedTask;
public Task DeleteAsync(
string programCode,
CancellationToken cancellationToken = default) => Task.CompletedTask;
}
private sealed class EmptyStockLookup : ILegacyStockLookup
{
public Task<LegacyStockSearchData> SearchAsync(
string query,
CancellationToken cancellationToken = default) =>
Task.FromResult(new LegacyStockSearchData(query, [], []));
}
}