feat: complete legacy UI and playout parity migration
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using System.Text.Json;
|
||||
using MBN_STOCK_WEBVIEW.LegacyApplication;
|
||||
using MBN_STOCK_WEBVIEW.LegacyBridge;
|
||||
using MMoneyCoderSharp.Data;
|
||||
|
||||
namespace MBN_STOCK_WEBVIEW.LegacyBridge.Tests;
|
||||
|
||||
@@ -37,6 +38,334 @@ public sealed class LegacyBridgeProtocolTests
|
||||
Assert.Equal(1_400, pointer.TimestampMilliseconds);
|
||||
}
|
||||
|
||||
[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 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.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_AcceptsOnlyIndexAndOpaqueActionId()
|
||||
{
|
||||
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.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<LegacySearchThemesIntent>(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<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\":\"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<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_AcceptsOnlyOpaqueIdsTitlesAndEmptyCommands()
|
||||
{
|
||||
Assert.True(LegacyBridgeProtocol.TryParseIntent(
|
||||
"{\"type\":\"refresh-named-playlists\",\"payload\":{}}",
|
||||
out var refresh,
|
||||
out _));
|
||||
Assert.IsType<LegacyRefreshNamedPlaylistsIntent>(refresh);
|
||||
|
||||
Assert.True(LegacyBridgeProtocol.TryParseIntent(
|
||||
"{\"type\":\"select-named-playlist\",\"payload\":{\"definitionId\":\"named-definition-AAAAAAAAAAAB\"}}",
|
||||
out var selection,
|
||||
out _));
|
||||
Assert.Equal(
|
||||
"named-definition-AAAAAAAAAAAB",
|
||||
Assert.IsType<LegacySelectNamedPlaylistIntent>(selection).DefinitionId);
|
||||
|
||||
Assert.True(LegacyBridgeProtocol.TryParseIntent(
|
||||
"{\"type\":\"create-named-playlist\",\"payload\":{\"title\":\"아침 방송 A^B\"}}",
|
||||
out var create,
|
||||
out _));
|
||||
Assert.Equal(
|
||||
"아침 방송 A^B",
|
||||
Assert.IsType<LegacyCreateNamedPlaylistIntent>(create).Title);
|
||||
|
||||
foreach (var type in new[]
|
||||
{
|
||||
"load-selected-named-playlist",
|
||||
"save-current-named-playlist",
|
||||
"delete-selected-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\":\"save-current-named-playlist\",\"payload\":{\"rows\":[]}}",
|
||||
out _,
|
||||
out _));
|
||||
Assert.False(LegacyBridgeProtocol.TryParseIntent(
|
||||
"{\"type\":\"create-named-playlist\",\"payload\":{\"title\":\" padded \"}}",
|
||||
out _,
|
||||
out _));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("https://legacy-parity.mbn.local/index.html", true)]
|
||||
[InlineData("https://legacy-parity.mbn.local.evil/index.html", false)]
|
||||
@@ -83,6 +412,94 @@ public sealed class LegacyBridgeProtocolTests
|
||||
Assert.Equal(
|
||||
"1열판기본_현재가",
|
||||
payload.GetProperty("cutRows")[0].GetProperty("rawLabel").GetString());
|
||||
Assert.False(payload.GetProperty("playlist")[0].GetProperty("isSelected").GetBoolean());
|
||||
Assert.True(payload.GetProperty("isBusy").GetBoolean());
|
||||
}
|
||||
|
||||
[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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
using MBN_STOCK_WEBVIEW.LegacyApplication;
|
||||
|
||||
namespace MBN_STOCK_WEBVIEW.LegacyBridge.Tests;
|
||||
|
||||
public sealed class LegacyComparisonBridgeTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("search-comparison-domestic", "{\"query\":\"삼성\"}", typeof(LegacySearchComparisonDomesticIntent))]
|
||||
[InlineData("search-comparison-world", "{\"query\":\"APPLE\"}", typeof(LegacySearchComparisonWorldIntent))]
|
||||
[InlineData("select-comparison-domestic", "{\"selectionId\":\"comparison-domestic-0001-0001\"}", typeof(LegacySelectComparisonDomesticIntent))]
|
||||
[InlineData("select-comparison-world", "{\"selectionId\":\"comparison-world-0001-0001\"}", typeof(LegacySelectComparisonWorldIntent))]
|
||||
[InlineData("select-comparison-fixed", "{\"targetId\":\"kospi\"}", typeof(LegacySelectComparisonFixedIntent))]
|
||||
[InlineData("choose-comparison-domestic", "{\"selectionId\":\"comparison-domestic-0001-0001\"}", typeof(LegacyChooseComparisonDomesticIntent))]
|
||||
[InlineData("choose-comparison-world", "{\"selectionId\":\"comparison-world-0001-0001\"}", typeof(LegacyChooseComparisonWorldIntent))]
|
||||
[InlineData("choose-comparison-fixed", "{\"targetId\":\"kospi\"}", typeof(LegacyChooseComparisonFixedIntent))]
|
||||
[InlineData("swap-comparison-targets", "{}", typeof(LegacySwapComparisonTargetsIntent))]
|
||||
[InlineData("clear-comparison-targets", "{}", typeof(LegacyClearComparisonTargetsIntent))]
|
||||
[InlineData("add-comparison-pair", "{}", typeof(LegacyAddComparisonPairIntent))]
|
||||
[InlineData("select-comparison-pair", "{\"rowId\":\"comparison-pair-00000001\"}", typeof(LegacySelectComparisonPairIntent))]
|
||||
[InlineData("move-comparison-pair", "{\"direction\":\"up\"}", typeof(LegacyMoveComparisonPairIntent))]
|
||||
[InlineData("delete-selected-comparison-pair", "{}", typeof(LegacyDeleteSelectedComparisonPairIntent))]
|
||||
[InlineData("delete-all-comparison-pairs", "{}", typeof(LegacyDeleteAllComparisonPairsIntent))]
|
||||
[InlineData("activate-comparison-action", "{\"actionId\":\"comparison-candle\"}", typeof(LegacyActivateComparisonActionIntent))]
|
||||
public void ComparisonIntentsAreClosed(string type, string payload, Type expected)
|
||||
{
|
||||
Assert.True(LegacyBridgeProtocol.TryParseIntent(
|
||||
$"{{\"type\":\"{type}\",\"payload\":{payload}}}",
|
||||
out var intent,
|
||||
out _));
|
||||
Assert.IsType(expected, intent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComparisonIntentRejectsUnexpectedOrRawIdentityFields()
|
||||
{
|
||||
Assert.False(LegacyBridgeProtocol.TryParseIntent(
|
||||
"{\"type\":\"choose-comparison-domestic\",\"payload\":{\"selectionId\":\"comparison-domestic-0001-0001\",\"stockCode\":\"005930\"}}",
|
||||
out _,
|
||||
out _));
|
||||
Assert.False(LegacyBridgeProtocol.TryParseIntent(
|
||||
"{\"type\":\"move-comparison-pair\",\"payload\":{\"direction\":\"sideways\"}}",
|
||||
out _,
|
||||
out _));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using System.Text.Json;
|
||||
using MBN_STOCK_WEBVIEW.LegacyApplication;
|
||||
|
||||
namespace MBN_STOCK_WEBVIEW.LegacyBridge.Tests;
|
||||
|
||||
public sealed class LegacyFixedSectionBatchBridgeTests
|
||||
{
|
||||
[Fact]
|
||||
public void Existing_fixed_section_intent_stays_strict_and_cannot_supply_resume_authority()
|
||||
{
|
||||
Assert.True(LegacyBridgeProtocol.TryParseIntent(
|
||||
"{\"type\":\"activate-fixed-section\",\"payload\":{\"sectionIndex\":6}}",
|
||||
out var parsed,
|
||||
out var error), error);
|
||||
Assert.Equal(6, Assert.IsType<LegacyActivateFixedSectionIntent>(parsed).SectionIndex);
|
||||
|
||||
Assert.False(LegacyBridgeProtocol.TryParseIntent(
|
||||
"{\"type\":\"activate-fixed-section\",\"payload\":{\"sectionIndex\":6,\"resume\":true}}",
|
||||
out _,
|
||||
out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void State_projects_only_native_fixed_section_progress_and_no_staged_rows()
|
||||
{
|
||||
var snapshot = new LegacyOperatorSnapshot(
|
||||
1,
|
||||
string.Empty,
|
||||
Array.Empty<LegacyStockResultRow>(),
|
||||
null,
|
||||
Array.Empty<LegacyCutViewRow>(),
|
||||
Array.Empty<LegacyOperatorPlaylistRow>(),
|
||||
null,
|
||||
"매매동향 섹션 수동 입력 1/3",
|
||||
LegacyOperatorStatusKind.Information,
|
||||
FixedSectionBatch: new LegacyFixedSectionBatchView(
|
||||
6,
|
||||
"매매동향",
|
||||
15,
|
||||
12,
|
||||
1,
|
||||
3,
|
||||
"fixed-245",
|
||||
"개인 순매도 상위(수동)"));
|
||||
|
||||
using var document = JsonDocument.Parse(LegacyBridgeProtocol.SerializeState(snapshot));
|
||||
var root = document.RootElement.GetProperty("payload");
|
||||
var batch = root.GetProperty("fixedSectionBatch");
|
||||
|
||||
Assert.Equal(6, batch.GetProperty("sectionIndex").GetInt32());
|
||||
Assert.Equal(15, batch.GetProperty("totalActionCount").GetInt32());
|
||||
Assert.Equal(12, batch.GetProperty("stagedActionCount").GetInt32());
|
||||
Assert.Equal("fixed-245", batch.GetProperty("pendingActionId").GetString());
|
||||
Assert.Empty(root.GetProperty("playlist").EnumerateArray());
|
||||
Assert.DoesNotContain("selection", batch.GetRawText(), StringComparison.OrdinalIgnoreCase);
|
||||
Assert.DoesNotContain("sceneAlias", batch.GetRawText(), StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
using System.Text.Json;
|
||||
using MBN_STOCK_WEBVIEW.LegacyApplication;
|
||||
using MMoneyCoderSharp.Data;
|
||||
|
||||
namespace MBN_STOCK_WEBVIEW.LegacyBridge.Tests;
|
||||
|
||||
public sealed class LegacyManualFinancialBridgeTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("revenue-composition", ManualFinancialScreenKind.RevenueComposition)]
|
||||
[InlineData("growth-metrics", ManualFinancialScreenKind.GrowthMetrics)]
|
||||
[InlineData("sales", ManualFinancialScreenKind.Sales)]
|
||||
[InlineData("operating-profit", ManualFinancialScreenKind.OperatingProfit)]
|
||||
public void Open_uses_closed_GraphE_screen_identity(
|
||||
string screen,
|
||||
ManualFinancialScreenKind expected)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(new
|
||||
{
|
||||
type = "open-manual-financial",
|
||||
payload = new { screen }
|
||||
});
|
||||
|
||||
Assert.True(LegacyBridgeProtocol.TryParseIntent(json, out var parsed, out _));
|
||||
Assert.Equal(expected, Assert.IsType<LegacyOpenManualFinancialIntent>(parsed).Screen);
|
||||
Assert.False(LegacyBridgeProtocol.TryParseIntent(
|
||||
"{\"type\":\"open-manual-financial\",\"payload\":{\"screen\":\"INPUT_SELL\"}}",
|
||||
out _,
|
||||
out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Save_parses_all_typed_record_shapes_without_accepting_DB_authority()
|
||||
{
|
||||
AssertSaveType<ManualRevenueCompositionRecord>(
|
||||
"""
|
||||
{"screen":"revenue-composition","stockName":"Alpha","baseDate":"2026.07","slices":[{"label":"A","percentageText":"25"},null,null,null,null]}
|
||||
""");
|
||||
AssertSaveType<ManualGrowthMetricsRecord>(
|
||||
"""
|
||||
{"screen":"growth-metrics","stockName":"Alpha","salesGrowth":[1,2,null,4],"operatingProfitGrowth":[5,6,7,8],"netAssetGrowth":[9,10,11,12],"netIncomeGrowth":[13,14,15,16],"periods":["1Q","2Q","3Q","4Q"]}
|
||||
""");
|
||||
AssertSaveType<ManualSalesRecord>(
|
||||
"""
|
||||
{"screen":"sales","stockName":"Alpha","quarters":[{"quarter":"1Q","value":1},{"quarter":"2Q","value":2},{"quarter":"3Q","value":3},{"quarter":"4Q","value":4},{"quarter":"5Q","value":5},{"quarter":"6Q","value":6}]}
|
||||
""");
|
||||
AssertSaveType<ManualOperatingProfitRecord>(
|
||||
"""
|
||||
{"screen":"operating-profit","stockName":"Alpha","quarters":[{"quarter":"1Q","value":1},{"quarter":"2Q","value":2},{"quarter":"3Q","value":3},{"quarter":"4Q","value":4},{"quarter":"5Q","value":5},{"quarter":"6Q","value":6}]}
|
||||
""");
|
||||
|
||||
Assert.False(LegacyBridgeProtocol.TryParseIntent(
|
||||
"""
|
||||
{"type":"save-manual-financial","payload":{"record":{"screen":"sales","stockName":"Alpha","rowVersion":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","quarters":[]}}}
|
||||
""",
|
||||
out _,
|
||||
out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CRUD_and_action_intents_accept_only_opaque_ids_or_empty_payloads()
|
||||
{
|
||||
Assert.True(LegacyBridgeProtocol.TryParseIntent(
|
||||
"{\"type\":\"load-manual-financial\",\"payload\":{\"resultId\":\"mf-row-0123456789abcdef\"}}",
|
||||
out var loaded,
|
||||
out _));
|
||||
Assert.IsType<LegacyLoadManualFinancialIntent>(loaded);
|
||||
|
||||
foreach (var type in new[]
|
||||
{
|
||||
"begin-manual-financial-create",
|
||||
"delete-manual-financial",
|
||||
"delete-all-manual-financial",
|
||||
"close-manual-financial"
|
||||
})
|
||||
{
|
||||
Assert.True(LegacyBridgeProtocol.TryParseIntent(
|
||||
$"{{\"type\":\"{type}\",\"payload\":{{}}}}",
|
||||
out _,
|
||||
out _));
|
||||
Assert.False(LegacyBridgeProtocol.TryParseIntent(
|
||||
$"{{\"type\":\"{type}\",\"payload\":{{\"table\":\"INPUT_SELL\"}}}}",
|
||||
out _,
|
||||
out _));
|
||||
}
|
||||
|
||||
Assert.True(LegacyBridgeProtocol.TryParseIntent(
|
||||
"{\"type\":\"activate-manual-financial-action\",\"payload\":{\"actionId\":\"sales\"}}",
|
||||
out var action,
|
||||
out _));
|
||||
Assert.Equal(
|
||||
"sales",
|
||||
Assert.IsType<LegacyActivateManualFinancialActionIntent>(action).ActionId);
|
||||
Assert.False(LegacyBridgeProtocol.TryParseIntent(
|
||||
"{\"type\":\"activate-manual-financial-action\",\"payload\":{\"actionId\":\"s5080\"}}",
|
||||
out _,
|
||||
out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GraphE_find_and_name_sort_require_strict_generation_bound_payloads()
|
||||
{
|
||||
Assert.True(LegacyBridgeProtocol.TryParseIntent(
|
||||
"{\"type\":\"find-manual-financial\",\"payload\":{\"query\":\"Alpha\",\"direction\":\"down\",\"generation\":7}}",
|
||||
out var find,
|
||||
out _));
|
||||
var parsedFind = Assert.IsType<LegacyFindManualFinancialIntent>(find);
|
||||
Assert.Equal(LegacyManualFinancialFindDirection.Down, parsedFind.Direction);
|
||||
Assert.Equal(7, parsedFind.Generation);
|
||||
|
||||
Assert.True(LegacyBridgeProtocol.TryParseIntent(
|
||||
"{\"type\":\"toggle-manual-financial-name-sort\",\"payload\":{\"generation\":7}}",
|
||||
out var sort,
|
||||
out _));
|
||||
Assert.Equal(7,
|
||||
Assert.IsType<LegacyToggleManualFinancialNameSortIntent>(sort).Generation);
|
||||
|
||||
Assert.False(LegacyBridgeProtocol.TryParseIntent(
|
||||
"{\"type\":\"find-manual-financial\",\"payload\":{\"query\":\"Alpha\",\"direction\":\"sideways\",\"generation\":7}}",
|
||||
out _,
|
||||
out _));
|
||||
Assert.False(LegacyBridgeProtocol.TryParseIntent(
|
||||
"{\"type\":\"toggle-manual-financial-name-sort\",\"payload\":{\"generation\":7,\"table\":\"INPUT_SELL\"}}",
|
||||
out _,
|
||||
out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Projection_exposes_editor_values_but_not_rowVersion_or_DB_scene_authority()
|
||||
{
|
||||
var manualService = new StaticManualService();
|
||||
var workflow = new LegacyManualFinancialWorkflow(
|
||||
manualService,
|
||||
new StaticStockService());
|
||||
var controller = new LegacyOperatorController(
|
||||
new EmptyStockLookup(),
|
||||
manualFinancialWorkflow: workflow);
|
||||
var opened = await controller.OpenManualFinancialAsync(ManualFinancialScreenKind.Sales);
|
||||
var loaded = await controller.LoadManualFinancialAsync(
|
||||
Assert.Single(opened.ManualFinancial!.Rows).ResultId);
|
||||
|
||||
using var document = JsonDocument.Parse(LegacyBridgeProtocol.SerializeState(loaded));
|
||||
var manual = document.RootElement.GetProperty("payload").GetProperty("manualFinancial");
|
||||
|
||||
Assert.Equal("sales", manual.GetProperty("screen").GetString());
|
||||
Assert.True(manual.GetProperty("generation").GetInt64() > 0);
|
||||
Assert.Equal("ascending", manual.GetProperty("nameSortDirection").GetString());
|
||||
Assert.Equal("Alpha", manual.GetProperty("selectedRecord").GetProperty("stockName").GetString());
|
||||
Assert.Equal("sales", manual.GetProperty("action").GetProperty("actionId").GetString());
|
||||
Assert.False(manual.TryGetProperty("rowVersion", out _));
|
||||
Assert.False(manual.TryGetProperty("tableName", out _));
|
||||
Assert.False(manual.TryGetProperty("builderKey", out _));
|
||||
Assert.False(manual.GetProperty("stockCandidates")[0].TryGetProperty("code", out _));
|
||||
}
|
||||
|
||||
private static void AssertSaveType<TRecord>(string recordJson)
|
||||
where TRecord : ManualFinancialRecord
|
||||
{
|
||||
var json = $"{{\"type\":\"save-manual-financial\",\"payload\":{{\"record\":{recordJson}}}}}";
|
||||
Assert.True(LegacyBridgeProtocol.TryParseIntent(json, out var parsed, out _));
|
||||
Assert.IsType<TRecord>(Assert.IsType<LegacySaveManualFinancialIntent>(parsed).Record);
|
||||
}
|
||||
|
||||
private sealed class EmptyStockLookup : ILegacyStockLookup
|
||||
{
|
||||
public Task<LegacyStockSearchData> SearchAsync(
|
||||
string query,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult(new LegacyStockSearchData(query, [], []));
|
||||
}
|
||||
|
||||
private sealed class StaticStockService : IStockSearchService
|
||||
{
|
||||
public Task<StockSearchResult> SearchAsync(
|
||||
string query,
|
||||
int maximumResults = LegacyStockSearchService.DefaultMaximumResults,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult(new StockSearchResult(
|
||||
query,
|
||||
DateTimeOffset.UtcNow,
|
||||
[new StockSearchItem(
|
||||
StockMarket.Kospi,
|
||||
DataSourceKind.Oracle,
|
||||
"Alpha",
|
||||
"000001")],
|
||||
false));
|
||||
}
|
||||
|
||||
private sealed class StaticManualService : IManualFinancialScreenService
|
||||
{
|
||||
private static readonly ManualFinancialSnapshot Row = new(
|
||||
new ManualSalesRecord(
|
||||
new ManualFinancialIdentity(ManualFinancialScreenKind.Sales, "Alpha"),
|
||||
Enumerable.Range(1, 6)
|
||||
.Select(index => new ManualQuarterValue($"{index}Q", index))
|
||||
.ToArray()),
|
||||
new string('A', 64));
|
||||
|
||||
public bool CanMutate => false;
|
||||
|
||||
public Task<ManualFinancialSearchResult> SearchAsync(
|
||||
ManualFinancialScreenKind screen,
|
||||
string query = "",
|
||||
int maximumResults = LegacyManualFinancialScreenService.DefaultMaximumResults,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult(new ManualFinancialSearchResult(
|
||||
screen,
|
||||
query,
|
||||
DateTimeOffset.UtcNow,
|
||||
screen == ManualFinancialScreenKind.Sales ? [Row] : [],
|
||||
false));
|
||||
|
||||
public Task<ManualFinancialSnapshot> GetAsync(
|
||||
ManualFinancialIdentity identity,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult(Row);
|
||||
|
||||
public Task<ManualFinancialMutationReceipt> CreateAsync(
|
||||
ManualFinancialRecord record,
|
||||
CancellationToken cancellationToken = default) => throw new NotSupportedException();
|
||||
|
||||
public Task<ManualFinancialMutationReceipt> UpdateAsync(
|
||||
ManualFinancialSnapshot expected,
|
||||
ManualFinancialRecord replacement,
|
||||
CancellationToken cancellationToken = default) => throw new NotSupportedException();
|
||||
|
||||
public Task<ManualFinancialMutationReceipt> DeleteAsync(
|
||||
ManualFinancialSnapshot expected,
|
||||
CancellationToken cancellationToken = default) => throw new NotSupportedException();
|
||||
|
||||
public Task<ManualFinancialMutationReceipt> DeleteAllAsync(
|
||||
ManualFinancialScreenKind screen,
|
||||
CancellationToken cancellationToken = default) => throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
using System.Text.Json;
|
||||
using MBN_STOCK_WEBVIEW.Core.Playout.Scenes;
|
||||
using MBN_STOCK_WEBVIEW.LegacyApplication;
|
||||
|
||||
namespace MBN_STOCK_WEBVIEW.LegacyBridge.Tests;
|
||||
|
||||
public sealed class LegacyManualListsBridgeTests
|
||||
{
|
||||
[Fact]
|
||||
public void OriginalOkActionHasOneClosedEmptyPayloadIntent()
|
||||
{
|
||||
Assert.True(LegacyBridgeProtocol.TryParseIntent(
|
||||
"{\"type\":\"confirm-manual-list\",\"payload\":{}}",
|
||||
out var intent,
|
||||
out _));
|
||||
Assert.IsType<LegacyConfirmManualListIntent>(intent);
|
||||
|
||||
Assert.False(LegacyBridgeProtocol.TryParseIntent(
|
||||
"{\"type\":\"confirm-manual-list\",\"payload\":{\"retry\":true}}",
|
||||
out _,
|
||||
out _));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("individual", LegacyManualListScreen.NetSell, S5025ManualAudience.Individual)]
|
||||
[InlineData("foreign", LegacyManualListScreen.NetSell, S5025ManualAudience.Foreign)]
|
||||
[InlineData("institution", LegacyManualListScreen.NetSell, S5025ManualAudience.Institution)]
|
||||
[InlineData("vi", LegacyManualListScreen.Vi, S5025ManualAudience.Individual)]
|
||||
public void Open_accepts_only_closed_screen_identifiers(
|
||||
string screen,
|
||||
LegacyManualListScreen expectedScreen,
|
||||
S5025ManualAudience expectedAudience)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(new
|
||||
{
|
||||
type = "open-manual-list",
|
||||
payload = new { screen }
|
||||
});
|
||||
|
||||
Assert.True(LegacyBridgeProtocol.TryParseIntent(json, out var intent, out _));
|
||||
var opened = Assert.IsType<LegacyOpenManualListIntent>(intent);
|
||||
Assert.Equal(expectedScreen, opened.Screen);
|
||||
Assert.Equal(expectedAudience, opened.Audience);
|
||||
Assert.False(LegacyBridgeProtocol.TryParseIntent(
|
||||
"{\"type\":\"open-manual-list\",\"payload\":{\"screen\":\"vi\",\"path\":\"C:\\\\Data\"}}",
|
||||
out _,
|
||||
out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Net_cell_accepts_one_opaque_row_and_safe_value_only()
|
||||
{
|
||||
Assert.True(LegacyBridgeProtocol.TryParseIntent(
|
||||
"{\"type\":\"set-manual-net-sell-cell\",\"payload\":{\"rowId\":\"manual-net-row-1\",\"field\":\"leftAmount\",\"value\":\"1,000\"}}",
|
||||
out var intent,
|
||||
out _));
|
||||
var cell = Assert.IsType<LegacySetManualNetSellCellIntent>(intent);
|
||||
Assert.Equal(LegacyManualNetSellField.LeftAmount, cell.Field);
|
||||
|
||||
foreach (var invalid in new[]
|
||||
{
|
||||
"{\"rowId\":\"manual-net-row-1\",\"field\":\"leftAmount\",\"value\":\"bad^value\"}",
|
||||
"{\"rowId\":\"manual-net-row-1\",\"field\":\"leftAmount\",\"value\":\"1\",\"audience\":\"FOREIGN\"}",
|
||||
"{\"rowId\":\"manual-net-row-1\",\"field\":\"code\",\"value\":\"005930\"}"
|
||||
})
|
||||
{
|
||||
Assert.False(LegacyBridgeProtocol.TryParseIntent(
|
||||
$"{{\"type\":\"set-manual-net-sell-cell\",\"payload\":{invalid}}}",
|
||||
out _,
|
||||
out _));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VI_mutations_accept_opaque_ids_and_never_codes_items_or_versions()
|
||||
{
|
||||
Assert.True(Parse("select-manual-vi-result", "{\"resultId\":\"manual-vi-result-0001\"}"));
|
||||
Assert.True(Parse("add-manual-vi-result", "{\"resultId\":\"manual-vi-result-0001\"}"));
|
||||
Assert.True(Parse("move-manual-vi-item", "{\"itemId\":\"manual-vi-item-0001\",\"direction\":\"up\"}"));
|
||||
Assert.True(Parse("delete-manual-vi-item", "{\"itemId\":\"manual-vi-item-0001\"}"));
|
||||
Assert.True(Parse("save-manual-vi", "{}"));
|
||||
Assert.False(Parse("save-manual-vi", "{\"version\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}"));
|
||||
Assert.False(Parse("add-manual-vi-result", "{\"code\":\"005930\",\"name\":\"삼성전자\"}"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void State_projects_display_rows_but_not_native_codes_versions_or_paths()
|
||||
{
|
||||
var viItems = Enumerable.Range(1, 9)
|
||||
.Select(index => new LegacyManualViItemView(
|
||||
$"manual-vi-item-{index}",
|
||||
index,
|
||||
$"VI종목{index}"))
|
||||
.ToArray();
|
||||
var manual = new LegacyManualListsSnapshot(
|
||||
true,
|
||||
LegacyManualListScreen.Vi,
|
||||
S5025ManualAudience.Individual,
|
||||
true,
|
||||
"READY",
|
||||
null,
|
||||
false,
|
||||
Array.Empty<LegacyManualNetSellRowView>(),
|
||||
false,
|
||||
viItems,
|
||||
2,
|
||||
true,
|
||||
"삼성",
|
||||
[new LegacyManualViSearchResultView(
|
||||
"manual-vi-result-1",
|
||||
"삼성전자",
|
||||
"코스피")],
|
||||
"조회 완료",
|
||||
LegacyOperatorStatusKind.Information,
|
||||
true,
|
||||
true)
|
||||
{
|
||||
SelectedSearchResultId = "manual-vi-result-1"
|
||||
};
|
||||
var snapshot = new LegacyOperatorSnapshot(
|
||||
1,
|
||||
string.Empty,
|
||||
Array.Empty<LegacyStockResultRow>(),
|
||||
null,
|
||||
Array.Empty<LegacyCutViewRow>(),
|
||||
Array.Empty<LegacyOperatorPlaylistRow>(),
|
||||
null,
|
||||
string.Empty,
|
||||
LegacyOperatorStatusKind.Neutral,
|
||||
ManualLists: manual);
|
||||
|
||||
var json = LegacyBridgeProtocol.SerializeState(snapshot);
|
||||
using var document = JsonDocument.Parse(json);
|
||||
var projected = document.RootElement.GetProperty("payload").GetProperty("manualLists");
|
||||
Assert.Equal(
|
||||
"manual-vi-result-1",
|
||||
projected.GetProperty("selectedSearchResultId").GetString());
|
||||
|
||||
Assert.True(projected.GetProperty("isOpen").GetBoolean());
|
||||
Assert.Equal(9, projected.GetProperty("viItems").GetArrayLength());
|
||||
Assert.Equal("VI종목1", projected.GetProperty("viItems")[0].GetProperty("name").GetString());
|
||||
Assert.Equal(2, projected.GetProperty("viPageCount").GetInt32());
|
||||
Assert.True(projected.GetProperty("viItemsAreFresh").GetBoolean());
|
||||
Assert.True(projected.GetProperty("canSave").GetBoolean());
|
||||
Assert.True(projected.GetProperty("canMaterializePlaylist").GetBoolean());
|
||||
Assert.False(projected.TryGetProperty("rowVersion", out _));
|
||||
Assert.DoesNotContain("005930", projected.GetRawText(), StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("path", projected.GetRawText(), StringComparison.OrdinalIgnoreCase);
|
||||
Assert.DoesNotContain("version", projected.GetRawText(), StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool Parse(string type, string payload) =>
|
||||
LegacyBridgeProtocol.TryParseIntent(
|
||||
$"{{\"type\":\"{type}\",\"payload\":{payload}}}",
|
||||
out _,
|
||||
out _);
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
using System.Text.Json;
|
||||
using MBN_STOCK_WEBVIEW.LegacyApplication;
|
||||
using MMoneyCoderSharp.Data;
|
||||
|
||||
namespace MBN_STOCK_WEBVIEW.LegacyBridge.Tests;
|
||||
|
||||
public sealed class LegacyOperatorCatalogBridgeTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("theme", LegacyOperatorCatalogEntity.Theme)]
|
||||
[InlineData("expert", LegacyOperatorCatalogEntity.Expert)]
|
||||
public void Open_accepts_only_the_closed_ThemeA_or_EList_identity(
|
||||
string entity,
|
||||
LegacyOperatorCatalogEntity expected)
|
||||
{
|
||||
Assert.True(Parse(
|
||||
JsonSerializer.Serialize(new
|
||||
{
|
||||
type = "open-operator-catalog",
|
||||
payload = new { entity }
|
||||
}),
|
||||
out var parsed));
|
||||
Assert.Equal(expected, Assert.IsType<LegacyOpenOperatorCatalogIntent>(parsed).Entity);
|
||||
|
||||
Assert.False(Parse(
|
||||
"""
|
||||
{"type":"open-operator-catalog","payload":{"entity":"SB_LIST"}}
|
||||
""",
|
||||
out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Catalog_selection_and_draft_changes_accept_only_opaque_ids()
|
||||
{
|
||||
Assert.IsType<LegacySelectOperatorCatalogResultIntent>(ParseRequired(
|
||||
"select-operator-catalog-result",
|
||||
"""{"resultId":"catalog-result-0123456789abcdef"}"""));
|
||||
Assert.IsType<LegacySelectOperatorCatalogStockIntent>(ParseRequired(
|
||||
"select-operator-catalog-stock",
|
||||
"""{"resultId":"catalog-stock-0123456789abcdef"}"""));
|
||||
Assert.IsType<LegacyMoveOperatorCatalogDraftRowIntent>(ParseRequired(
|
||||
"move-operator-catalog-draft-row",
|
||||
"""{"rowId":"catalog-draft-0123456789abcdef","direction":"up"}"""));
|
||||
Assert.IsType<LegacyRemoveOperatorCatalogDraftRowIntent>(ParseRequired(
|
||||
"remove-operator-catalog-draft-row",
|
||||
"""{"rowId":"catalog-draft-0123456789abcdef"}"""));
|
||||
|
||||
Assert.False(Parse(
|
||||
"""
|
||||
{"type":"select-operator-catalog-result","payload":{"resultId":"catalog-result-0123456789abcdef","themeCode":"00000001"}}
|
||||
""",
|
||||
out _));
|
||||
Assert.False(Parse(
|
||||
"""
|
||||
{"type":"select-operator-catalog-stock","payload":{"stockCode":"005930"}}
|
||||
""",
|
||||
out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_save_delete_and_add_are_closed_and_typed()
|
||||
{
|
||||
Assert.IsType<LegacyBeginCreateThemeCatalogIntent>(ParseRequired(
|
||||
"begin-create-theme-catalog",
|
||||
"""{"market":"nxt"}"""));
|
||||
Assert.IsType<LegacyBeginCreateExpertCatalogIntent>(ParseRequired(
|
||||
"begin-create-expert-catalog",
|
||||
"{}"));
|
||||
Assert.Null(Assert.IsType<LegacyAddOperatorCatalogStockIntent>(ParseRequired(
|
||||
"add-operator-catalog-stock",
|
||||
"""{"buyAmount":null}""")).BuyAmount);
|
||||
Assert.Equal(70_000, Assert.IsType<LegacyAddOperatorCatalogStockIntent>(ParseRequired(
|
||||
"add-operator-catalog-stock",
|
||||
"""{"buyAmount":70000}""")).BuyAmount);
|
||||
Assert.Equal("김전문", Assert.IsType<LegacySaveOperatorCatalogIntent>(ParseRequired(
|
||||
"save-operator-catalog",
|
||||
"""{"name":"김전문"}""")).Name);
|
||||
Assert.IsType<LegacyDeleteOperatorCatalogIntent>(ParseRequired(
|
||||
"delete-operator-catalog",
|
||||
"{}"));
|
||||
|
||||
Assert.False(Parse(
|
||||
"""
|
||||
{"type":"save-operator-catalog","payload":{"name":"김전문","expertCode":"0001"}}
|
||||
""",
|
||||
out _));
|
||||
Assert.False(Parse(
|
||||
"""
|
||||
{"type":"add-operator-catalog-stock","payload":{"buyAmount":1.5}}
|
||||
""",
|
||||
out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Projection_exposes_display_data_and_opaque_ids_without_DB_identity_authority()
|
||||
{
|
||||
var catalog = new LegacyOperatorCatalogWorkflowSnapshot(
|
||||
4,
|
||||
true,
|
||||
LegacyOperatorCatalogEntity.Theme,
|
||||
LegacyOperatorCatalogMode.Edit,
|
||||
"AI",
|
||||
ThemeSession.PreMarket,
|
||||
ThemeMarket.Krx,
|
||||
[
|
||||
new LegacyOperatorCatalogResultView(
|
||||
"catalog-result-0123456789abcdef",
|
||||
"00000001",
|
||||
"AI 반도체",
|
||||
"KRX / Oracle",
|
||||
true)
|
||||
],
|
||||
false,
|
||||
"catalog-result-0123456789abcdef",
|
||||
"00000001",
|
||||
"AI 반도체",
|
||||
[
|
||||
new LegacyOperatorCatalogDraftRowView(
|
||||
"catalog-draft-0123456789abcdef",
|
||||
"삼성전자",
|
||||
"KOSPI",
|
||||
null,
|
||||
0)
|
||||
],
|
||||
"삼성",
|
||||
[
|
||||
new LegacyOperatorCatalogStockResultView(
|
||||
"catalog-stock-0123456789abcdef",
|
||||
"삼성전자",
|
||||
"KOSPI",
|
||||
true,
|
||||
true)
|
||||
],
|
||||
false,
|
||||
"catalog-stock-0123456789abcdef",
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
"편집 중",
|
||||
LegacyOperatorCatalogStatusKind.Information);
|
||||
var snapshot = new LegacyOperatorSnapshot(
|
||||
1,
|
||||
string.Empty,
|
||||
[],
|
||||
null,
|
||||
[],
|
||||
[],
|
||||
null,
|
||||
string.Empty,
|
||||
LegacyOperatorStatusKind.Neutral,
|
||||
OperatorCatalog: catalog);
|
||||
|
||||
var json = LegacyBridgeProtocol.SerializeState(snapshot);
|
||||
|
||||
Assert.Contains("catalog-result-0123456789abcdef", json, StringComparison.Ordinal);
|
||||
Assert.Contains("codeLabel", json, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("themeCode", json, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("expertCode", json, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("stockCode", json, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("itemCode", json, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("expectedIdentity", json, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static LegacyUiIntent ParseRequired(string type, string payload)
|
||||
{
|
||||
var json = "{\"type\":\"" + type + "\",\"payload\":" + payload + "}";
|
||||
Assert.True(Parse(json, out var intent));
|
||||
return Assert.IsAssignableFrom<LegacyUiIntent>(intent);
|
||||
}
|
||||
|
||||
private static bool Parse(string json, out LegacyUiIntent? intent) =>
|
||||
LegacyBridgeProtocol.TryParseIntent(json, out intent, out _);
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
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 LegacyOverseasBridgeTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(
|
||||
"{\"type\":\"select-overseas-fixed-index\",\"payload\":{\"targetId\":\"nasdaq\"}}",
|
||||
typeof(LegacySelectOverseasFixedIndexIntent))]
|
||||
[InlineData(
|
||||
"{\"type\":\"search-overseas-industries\",\"payload\":{\"query\":\"반도체\"}}",
|
||||
typeof(LegacySearchOverseasIndustriesIntent))]
|
||||
[InlineData(
|
||||
"{\"type\":\"search-overseas-stocks\",\"payload\":{\"query\":\"Apple\"}}",
|
||||
typeof(LegacySearchOverseasStocksIntent))]
|
||||
[InlineData(
|
||||
"{\"type\":\"select-overseas-industry\",\"payload\":{\"selectionId\":\"overseas-industry-00000001-0001\"}}",
|
||||
typeof(LegacySelectOverseasIndustryIntent))]
|
||||
[InlineData(
|
||||
"{\"type\":\"select-overseas-stock\",\"payload\":{\"selectionId\":\"overseas-stock-00000001-0001\"}}",
|
||||
typeof(LegacySelectOverseasStockIntent))]
|
||||
[InlineData(
|
||||
"{\"type\":\"activate-overseas-action\",\"payload\":{\"actionId\":\"stock-candle-20d\"}}",
|
||||
typeof(LegacyActivateOverseasActionIntent))]
|
||||
public void ParsesStrictOverseasIntents(string json, Type expectedType)
|
||||
{
|
||||
Assert.True(LegacyBridgeProtocol.TryParseIntent(json, out var intent, out var error));
|
||||
Assert.Null(error);
|
||||
Assert.IsType(expectedType, intent);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("{\"type\":\"select-overseas-fixed-index\",\"payload\":{\"targetId\":\"not-registered\"}}")]
|
||||
[InlineData("{\"type\":\"activate-overseas-action\",\"payload\":{\"actionId\":\"8061\"}}")]
|
||||
[InlineData("{\"type\":\"select-overseas-stock\",\"payload\":{\"selectionId\":\"../../stock\"}}")]
|
||||
[InlineData("{\"type\":\"search-overseas-industries\",\"payload\":{\"query\":\"bad\\u0001query\"}}")]
|
||||
[InlineData("{\"type\":\"search-overseas-stocks\",\"payload\":{\"query\":\"Apple\",\"symbol\":\"NAS@AAPL\"}}")]
|
||||
[InlineData("{\"type\":\"activate-overseas-action\",\"payload\":{\"actionId\":12}}")]
|
||||
public void RejectsUnknownMalformedAndAuthorityBearingOverseasIntents(string json)
|
||||
{
|
||||
Assert.False(LegacyBridgeProtocol.TryParseIntent(json, out var intent, out var error));
|
||||
Assert.Null(intent);
|
||||
Assert.NotNull(error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProjectionExposesDisplayStateAndOpaqueIdsButNoSceneAuthority()
|
||||
{
|
||||
var workflow = new LegacyOverseasSelectionWorkflow(
|
||||
new StaticIndustryService(),
|
||||
new StaticStockService());
|
||||
workflow.SelectFixedIndex("nasdaq");
|
||||
var industries = await workflow.SearchIndustriesAsync("반도체");
|
||||
workflow.SelectIndustry(industries.Industry.Results[0].SelectionId);
|
||||
var stocks = await workflow.SearchStocksAsync("Apple");
|
||||
workflow.SelectStock(stocks.Stock.Results[0].SelectionId);
|
||||
var snapshot = new LegacyOperatorSnapshot(
|
||||
7,
|
||||
string.Empty,
|
||||
[],
|
||||
null,
|
||||
[],
|
||||
[],
|
||||
null,
|
||||
string.Empty,
|
||||
LegacyOperatorStatusKind.Neutral,
|
||||
Overseas: workflow.Current);
|
||||
|
||||
var json = LegacyBridgeProtocol.SerializeState(snapshot);
|
||||
using var document = JsonDocument.Parse(json);
|
||||
var overseas = document.RootElement.GetProperty("payload").GetProperty("overseas");
|
||||
|
||||
Assert.Equal(10, overseas.GetProperty("fixedIndexTargets").GetArrayLength());
|
||||
Assert.Equal("nasdaq", overseas.GetProperty("selectedFixedIndexId").GetString());
|
||||
Assert.Equal("나스닥", overseas.GetProperty("selectedFixedIndexName").GetString());
|
||||
Assert.Equal(
|
||||
"overseas-industry-00000001-0001",
|
||||
overseas.GetProperty("industry").GetProperty("selectedId").GetString());
|
||||
Assert.Equal(
|
||||
"overseas-stock-00000001-0001",
|
||||
overseas.GetProperty("stock").GetProperty("selectedId").GetString());
|
||||
Assert.Equal(12, overseas.GetProperty("actions").GetArrayLength());
|
||||
Assert.All(
|
||||
overseas.GetProperty("actions").EnumerateArray(),
|
||||
action => Assert.True(action.GetProperty("isAvailable").GetBoolean()));
|
||||
Assert.DoesNotContain("builderKey", json, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.DoesNotContain("sceneAlias", json, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.DoesNotContain("dataCode", json, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.DoesNotContain("NAS@AAPL", json, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("NAS@SOX", json, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private sealed class StaticIndustryService : IOverseasIndustryIndexSearchService
|
||||
{
|
||||
public Task<OverseasIndustryIndexSearchResult> SearchAsync(
|
||||
string query,
|
||||
int maximumResults = LegacyOverseasIndustryIndexSearchService.DefaultMaximumResults,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult(new OverseasIndustryIndexSearchResult(
|
||||
query,
|
||||
DateTimeOffset.Parse("2026-07-15T10:00:00+09:00"),
|
||||
[new OverseasIndustryIndexItem("반도체", "US Semiconductor", "NAS@SOX")],
|
||||
IsTruncated: false));
|
||||
}
|
||||
|
||||
private sealed class StaticStockService : IWorldStockSearchService
|
||||
{
|
||||
public Task<WorldStockSearchResult> SearchAsync(
|
||||
string query,
|
||||
int maximumResults = LegacyWorldStockSearchService.DefaultMaximumResults,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult(new WorldStockSearchResult(
|
||||
query,
|
||||
DateTimeOffset.Parse("2026-07-15T10:00:00+09:00"),
|
||||
[new WorldStockSearchItem("Apple", "NAS@AAPL", "US")],
|
||||
IsTruncated: false));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
using System.Text.Json;
|
||||
using MBN_STOCK_WEBVIEW.Core.Playout;
|
||||
using MBN_STOCK_WEBVIEW.Core.Playout.Scenes;
|
||||
using MBN_STOCK_WEBVIEW.LegacyApplication;
|
||||
using MBN_STOCK_WEBVIEW.LegacyBridge;
|
||||
|
||||
namespace MBN_STOCK_WEBVIEW.LegacyBridge.Tests;
|
||||
|
||||
public sealed class LegacyPlayoutBridgeTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("prepare-playout", LegacyOperatorPlayoutCommand.Prepare)]
|
||||
[InlineData("take-in", LegacyOperatorPlayoutCommand.TakeIn)]
|
||||
[InlineData("next-playout", LegacyOperatorPlayoutCommand.Next)]
|
||||
[InlineData("take-out", LegacyOperatorPlayoutCommand.TakeOut)]
|
||||
public void CommandIntents_ParseClosedEmptyPayload(
|
||||
string type,
|
||||
LegacyOperatorPlayoutCommand expected)
|
||||
{
|
||||
var parsed = LegacyBridgeProtocol.TryParseIntent(
|
||||
"{\"type\":\"" + type + "\",\"payload\":{}}",
|
||||
out var intent,
|
||||
out var error);
|
||||
|
||||
Assert.True(parsed, error);
|
||||
var command = Assert.IsType<LegacyExecutePlayoutIntent>(intent);
|
||||
Assert.Equal(expected, command.Command);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("prepare-playout")]
|
||||
[InlineData("take-in")]
|
||||
[InlineData("next-playout")]
|
||||
[InlineData("take-out")]
|
||||
[InlineData("toggle-background")]
|
||||
[InlineData("choose-background")]
|
||||
public void EmptyPlayoutIntents_RejectInjectedProperties(string type)
|
||||
{
|
||||
Assert.False(LegacyBridgeProtocol.TryParseIntent(
|
||||
"{\"type\":\"" + type +
|
||||
"\",\"payload\":{\"scene\":\"5001\"}}",
|
||||
out _,
|
||||
out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompositionIntents_ParseOnlyTrustedShape()
|
||||
{
|
||||
Assert.True(LegacyBridgeProtocol.TryParseIntent(
|
||||
"""{"type":"set-fade-duration","payload":{"duration":6}}""",
|
||||
out var fade,
|
||||
out var fadeError), fadeError);
|
||||
Assert.Equal(6, Assert.IsType<LegacySetFadeDurationIntent>(fade).Duration);
|
||||
|
||||
Assert.True(LegacyBridgeProtocol.TryParseIntent(
|
||||
"""{"type":"toggle-background","payload":{}}""",
|
||||
out var toggle,
|
||||
out var toggleError), toggleError);
|
||||
Assert.IsType<LegacyToggleBackgroundIntent>(toggle);
|
||||
|
||||
Assert.True(LegacyBridgeProtocol.TryParseIntent(
|
||||
"""{"type":"choose-background","payload":{}}""",
|
||||
out var choose,
|
||||
out var chooseError), chooseError);
|
||||
Assert.IsType<LegacyChooseBackgroundIntent>(choose);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("-1")]
|
||||
[InlineData("61")]
|
||||
[InlineData("1.5")]
|
||||
[InlineData("\"6\"")]
|
||||
[InlineData("null")]
|
||||
public void FadeDuration_RejectsNonCanonicalValues(string value)
|
||||
{
|
||||
Assert.False(LegacyBridgeProtocol.TryParseIntent(
|
||||
"{\"type\":\"set-fade-duration\",\"payload\":{\"duration\":" +
|
||||
value + "}}",
|
||||
out _,
|
||||
out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SerializedState_ProjectsAuthoritativePlayoutState()
|
||||
{
|
||||
var snapshot = new LegacyOperatorSnapshot(
|
||||
1,
|
||||
string.Empty,
|
||||
[],
|
||||
null,
|
||||
[],
|
||||
[],
|
||||
null,
|
||||
string.Empty,
|
||||
LegacyOperatorStatusKind.Neutral);
|
||||
var playout = new LegacyOperatorPlayoutSnapshot(
|
||||
Mode: PlayoutMode.DryRun,
|
||||
ConnectionState: PlayoutConnectionState.DryRunReady,
|
||||
Phase: LegacyOperatorPlayoutPhase.Prepared,
|
||||
IsProcessRunning: true,
|
||||
IsConnected: true,
|
||||
IsCommandAvailable: true,
|
||||
LiveTakeInAllowed: true,
|
||||
IsPlayCompletionPending: false,
|
||||
OutcomeUnknown: false,
|
||||
PreparedCode: "5001",
|
||||
OnAirCode: null,
|
||||
CurrentCueIndexZeroBased: 0,
|
||||
CurrentEntryId: "entry-1",
|
||||
BuilderKey: "s5001",
|
||||
PageIndexZeroBased: 0,
|
||||
PageCount: 2,
|
||||
PageSize: 5,
|
||||
ItemCount: 8,
|
||||
CurrentPageItemCount: 5,
|
||||
IsLastPage: false,
|
||||
NextKind: LegacyWorkflowNextKind.PageNext,
|
||||
IsBusy: false,
|
||||
RefreshActive: false,
|
||||
RefreshCompletedCount: 0,
|
||||
RefreshMaximumCount: 1,
|
||||
RefreshLimitReached: false,
|
||||
FadeDuration: 6,
|
||||
BackgroundEnabled: true,
|
||||
BackgroundFileName: "기본.vrv",
|
||||
CanChangeBackground: false,
|
||||
Message: "준비 완료");
|
||||
|
||||
using var document = JsonDocument.Parse(
|
||||
LegacyBridgeProtocol.SerializeState(snapshot, playout: playout));
|
||||
var projected = document.RootElement.GetProperty("payload").GetProperty("playout");
|
||||
|
||||
Assert.Equal("dryRun", projected.GetProperty("mode").GetString());
|
||||
Assert.Equal("dryRunReady", projected.GetProperty("connectionState").GetString());
|
||||
Assert.Equal("prepared", projected.GetProperty("phase").GetString());
|
||||
Assert.Equal("5001", projected.GetProperty("preparedCode").GetString());
|
||||
Assert.Equal("pageNext", projected.GetProperty("nextKind").GetString());
|
||||
Assert.Equal(2, projected.GetProperty("pageCount").GetInt32());
|
||||
Assert.Equal("기본.vrv", projected.GetProperty("backgroundFileName").GetString());
|
||||
Assert.False(projected.TryGetProperty("currentEntryId", out _));
|
||||
Assert.False(projected.TryGetProperty("builderKey", out _));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
using System.Text.Json;
|
||||
using MBN_STOCK_WEBVIEW.LegacyApplication;
|
||||
|
||||
namespace MBN_STOCK_WEBVIEW.LegacyBridge.Tests;
|
||||
|
||||
public sealed class LegacyRowDoubleClickBridgeTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(
|
||||
"activate-manual-financial-result",
|
||||
"resultId",
|
||||
"mf-row-0123456789abcdef",
|
||||
typeof(LegacyActivateManualFinancialResultIntent))]
|
||||
[InlineData(
|
||||
"activate-theme-result",
|
||||
"resultId",
|
||||
"theme-result-001",
|
||||
typeof(LegacyActivateThemeResultIntent))]
|
||||
[InlineData(
|
||||
"load-named-playlist-by-id",
|
||||
"definitionId",
|
||||
"named-definition-AAAAAAAAAAAB",
|
||||
typeof(LegacyLoadNamedPlaylistByIdIntent))]
|
||||
[InlineData(
|
||||
"save-current-named-playlist-to",
|
||||
"definitionId",
|
||||
"named-definition-AAAAAAAAAAAB",
|
||||
typeof(LegacySaveCurrentNamedPlaylistToIntent))]
|
||||
public void Atomic_row_commands_accept_one_opaque_identity_only(
|
||||
string command,
|
||||
string property,
|
||||
string value,
|
||||
Type expectedType)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(new
|
||||
{
|
||||
type = command,
|
||||
payload = new Dictionary<string, string> { [property] = value }
|
||||
});
|
||||
|
||||
Assert.True(LegacyBridgeProtocol.TryParseIntent(json, out var parsed, out _));
|
||||
Assert.IsType(expectedType, parsed);
|
||||
Assert.False(LegacyBridgeProtocol.TryParseIntent(
|
||||
$"{{\"type\":\"{command}\",\"payload\":{{\"{property}\":\"{value}\",\"programCode\":\"00000001\"}}}}",
|
||||
out _,
|
||||
out _));
|
||||
Assert.False(LegacyBridgeProtocol.TryParseIntent(
|
||||
$"{{\"type\":\"{command}\",\"payload\":{{\"{property}\":\"../unsafe\"}}}}",
|
||||
out _,
|
||||
out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Projection_exposes_only_closed_command_receipt_and_opaque_target()
|
||||
{
|
||||
var snapshot = new LegacyOperatorSnapshot(
|
||||
1,
|
||||
string.Empty,
|
||||
[],
|
||||
null,
|
||||
[],
|
||||
[],
|
||||
null,
|
||||
string.Empty,
|
||||
LegacyOperatorStatusKind.Information,
|
||||
CommandResult: new LegacyOperatorCommandResult(
|
||||
7,
|
||||
"load-named-playlist-by-id",
|
||||
"named-definition-AAAAAAAAAAAB",
|
||||
true));
|
||||
|
||||
using var document = JsonDocument.Parse(
|
||||
LegacyBridgeProtocol.SerializeState(snapshot));
|
||||
var result = document.RootElement.GetProperty("payload")
|
||||
.GetProperty("commandResult");
|
||||
|
||||
Assert.Equal(7, result.GetProperty("sequence").GetInt64());
|
||||
Assert.Equal("load-named-playlist-by-id",
|
||||
result.GetProperty("command").GetString());
|
||||
Assert.Equal("named-definition-AAAAAAAAAAAB",
|
||||
result.GetProperty("targetId").GetString());
|
||||
Assert.True(result.GetProperty("succeeded").GetBoolean());
|
||||
Assert.DoesNotContain("programCode", result.GetRawText(),
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using MBN_STOCK_WEBVIEW.LegacyBridge;
|
||||
|
||||
namespace MBN_STOCK_WEBVIEW.LegacyBridge.Tests;
|
||||
|
||||
public sealed class LegacySectionBatchBridgeTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("activate-industry-section", typeof(LegacyActivateIndustrySectionIntent))]
|
||||
[InlineData("activate-comparison-section", typeof(LegacyActivateComparisonSectionIntent))]
|
||||
public void SectionBatchIntentAcceptsOnlyOneBoundedOpaqueIndex(
|
||||
string type,
|
||||
Type expectedType)
|
||||
{
|
||||
Assert.True(LegacyBridgeProtocol.TryParseIntent(
|
||||
$"{{\"type\":\"{type}\",\"payload\":{{\"sectionIndex\":1}}}}",
|
||||
out var intent,
|
||||
out _));
|
||||
Assert.IsType(expectedType, intent);
|
||||
|
||||
Assert.False(LegacyBridgeProtocol.TryParseIntent(
|
||||
$"{{\"type\":\"{type}\",\"payload\":{{\"sectionIndex\":64}}}}",
|
||||
out _,
|
||||
out _));
|
||||
Assert.False(LegacyBridgeProtocol.TryParseIntent(
|
||||
$"{{\"type\":\"{type}\",\"payload\":{{\"sectionIndex\":1,\"actions\":[]}}}}",
|
||||
out _,
|
||||
out _));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using MBN_STOCK_WEBVIEW.LegacyApplication;
|
||||
|
||||
namespace MBN_STOCK_WEBVIEW.LegacyBridge.Tests;
|
||||
|
||||
public sealed class LegacyTabActivationBridgeTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("theme", LegacyOperatorTabId.Theme)]
|
||||
[InlineData("expert", LegacyOperatorTabId.Expert)]
|
||||
public void SelectTab_CarriesOnlyClosedTabIdentity(
|
||||
string tabId,
|
||||
LegacyOperatorTabId expected)
|
||||
{
|
||||
Assert.True(LegacyBridgeProtocol.TryParseIntent(
|
||||
$"{{\"type\":\"select-tab\",\"payload\":{{\"tabId\":\"{tabId}\"}}}}",
|
||||
out var parsed,
|
||||
out _));
|
||||
|
||||
Assert.Equal(expected, Assert.IsType<LegacySelectTabIntent>(parsed).TabId);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("theme", "query", "")]
|
||||
[InlineData("theme", "nxtSession", "preMarket")]
|
||||
[InlineData("expert", "query", "")]
|
||||
public void SelectTab_RejectsWebSuppliedInitialLoadParameters(
|
||||
string tabId,
|
||||
string extraName,
|
||||
string extraValue)
|
||||
{
|
||||
var json = "{\"type\":\"select-tab\",\"payload\":{" +
|
||||
$"\"tabId\":\"{tabId}\",\"{extraName}\":\"{extraValue}\"}}}}";
|
||||
|
||||
Assert.False(LegacyBridgeProtocol.TryParseIntent(json, out _, out _));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace MBN_STOCK_WEBVIEW.LegacyBridge.Tests;
|
||||
|
||||
public sealed class LegacyUc4Uc6CatalogBridgeTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("begin-uc4-theme-catalog", typeof(LegacyBeginUc4ThemeCatalogIntent))]
|
||||
[InlineData("edit-uc4-selected-theme", typeof(LegacyEditUc4SelectedThemeIntent))]
|
||||
[InlineData("delete-uc4-selected-theme", typeof(LegacyDeleteUc4SelectedThemeIntent))]
|
||||
[InlineData("begin-uc6-expert-catalog", typeof(LegacyBeginUc6ExpertCatalogIntent))]
|
||||
[InlineData("edit-uc6-selected-expert", typeof(LegacyEditUc6SelectedExpertIntent))]
|
||||
[InlineData("delete-uc6-selected-expert", typeof(LegacyDeleteUc6SelectedExpertIntent))]
|
||||
public void Original_Uc4_Uc6_entries_accept_only_an_empty_payload(
|
||||
string command,
|
||||
Type expectedType)
|
||||
{
|
||||
var valid = JsonSerializer.Serialize(new
|
||||
{
|
||||
type = command,
|
||||
payload = new { }
|
||||
});
|
||||
Assert.True(LegacyBridgeProtocol.TryParseIntent(
|
||||
valid,
|
||||
out var parsed,
|
||||
out _));
|
||||
Assert.IsType(expectedType, parsed);
|
||||
|
||||
var invalid = JsonSerializer.Serialize(new
|
||||
{
|
||||
type = command,
|
||||
payload = new { themeCode = "00000001" }
|
||||
});
|
||||
Assert.False(LegacyBridgeProtocol.TryParseIntent(
|
||||
invalid,
|
||||
out _,
|
||||
out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Recommendation_amount_edit_accepts_one_opaque_row_and_web_safe_integer()
|
||||
{
|
||||
var valid = JsonSerializer.Serialize(new
|
||||
{
|
||||
type = "set-operator-catalog-draft-buy-amount",
|
||||
payload = new { rowId = "catalog-draft-0123456789abcdef", buyAmount = 71_000 }
|
||||
});
|
||||
|
||||
Assert.True(LegacyBridgeProtocol.TryParseIntent(valid, out var parsed, out _));
|
||||
var amount = Assert.IsType<LegacySetOperatorCatalogDraftBuyAmountIntent>(parsed);
|
||||
Assert.Equal(71_000, amount.BuyAmount);
|
||||
|
||||
foreach (var invalid in new[]
|
||||
{
|
||||
"{\"type\":\"set-operator-catalog-draft-buy-amount\",\"payload\":{\"rowId\":\"../unsafe\",\"buyAmount\":71000}}",
|
||||
"{\"type\":\"set-operator-catalog-draft-buy-amount\",\"payload\":{\"rowId\":\"catalog-draft-0123456789abcdef\",\"buyAmount\":0}}",
|
||||
"{\"type\":\"set-operator-catalog-draft-buy-amount\",\"payload\":{\"rowId\":\"catalog-draft-0123456789abcdef\",\"buyAmount\":71000,\"expertCode\":\"0001\"}}"
|
||||
})
|
||||
{
|
||||
Assert.False(LegacyBridgeProtocol.TryParseIntent(invalid, out _, out _));
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("null", null)]
|
||||
[InlineData("71000", 71_000)]
|
||||
public void Stock_double_click_accepts_only_opaque_result_and_optional_amount(
|
||||
string amountJson,
|
||||
int? expected)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(new
|
||||
{
|
||||
type = "activate-operator-catalog-stock",
|
||||
payload = new
|
||||
{
|
||||
resultId = "catalog-stock-0123456789abcdef",
|
||||
buyAmount = amountJson == "null" ? (decimal?)null : decimal.Parse(amountJson)
|
||||
}
|
||||
});
|
||||
|
||||
Assert.True(LegacyBridgeProtocol.TryParseIntent(json, out var parsed, out _));
|
||||
var activation = Assert.IsType<LegacyActivateOperatorCatalogStockIntent>(parsed);
|
||||
Assert.Equal(expected.HasValue ? Convert.ToDecimal(expected.Value) : null,
|
||||
activation.BuyAmount);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user