796 lines
34 KiB
C#
796 lines
34 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);
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseExpertInitialConsonantQuery_PreservesCompatibilityJamo()
|
|
{
|
|
Assert.True(LegacyBridgeProtocol.TryParseIntent(
|
|
"{\"type\":\"search-experts\",\"payload\":{\"query\":\"ㄱㅈㅁ\"}}",
|
|
out var intent,
|
|
out _));
|
|
|
|
Assert.Equal("ㄱㅈㅁ", Assert.IsType<LegacySearchExpertsIntent>(intent).Query);
|
|
}
|
|
|
|
[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, [], []));
|
|
}
|
|
}
|