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

198 lines
10 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 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(
0,
overseas.GetProperty("stock").GetProperty("isolatedInvalidRowCount").GetInt32());
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("nationCode", json, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("NAS@AAPL", json, StringComparison.Ordinal);
Assert.DoesNotContain("NAS@SOX", json, StringComparison.Ordinal);
}
[Fact]
public async Task ProjectionKeepsNonUsTwRowOpaqueAndReportsItAvailable()
{
var workflow = new LegacyOverseasSelectionWorkflow(
new StaticIndustryService(),
new StaticStockService([new WorldStockSearchItem("London Stock", "LNS@LON", "GB")]));
var searched = await workflow.SearchStocksAsync("London");
workflow.SelectStock(Assert.Single(searched.Stock.Results).SelectionId);
var snapshot = new LegacyOperatorSnapshot(
7, string.Empty, [], null, [], [], null, string.Empty,
LegacyOperatorStatusKind.Neutral, Overseas: workflow.Current);
using var document = JsonDocument.Parse(LegacyBridgeProtocol.SerializeState(snapshot));
var overseas = document.RootElement.GetProperty("payload").GetProperty("overseas");
var result = Assert.Single(overseas.GetProperty("stock").GetProperty("results").EnumerateArray());
Assert.Equal("London Stock", result.GetProperty("displayName").GetString());
Assert.False(result.TryGetProperty("nationCode", out _));
var action = overseas.GetProperty("actions").EnumerateArray().Single(value =>
value.GetProperty("actionId").GetString() == "stock-current");
Assert.True(action.GetProperty("isAvailable").GetBoolean());
Assert.Equal(JsonValueKind.Null, action.GetProperty("reason").ValueKind);
Assert.DoesNotContain("LNS@LON", document.RootElement.GetRawText(), StringComparison.Ordinal);
}
[Fact]
public async Task ProjectionKeepsThreeLetterNationOpaqueAndFailsClosed()
{
var workflow = new LegacyOverseasSelectionWorkflow(
new StaticIndustryService(),
new StaticStockService([new WorldStockSearchItem("Unsupported", "LNS@UNSUPPORTED", "GBR")]));
var searched = await workflow.SearchStocksAsync("unsupported");
workflow.SelectStock(Assert.Single(searched.Stock.Results).SelectionId);
var snapshot = new LegacyOperatorSnapshot(
7, string.Empty, [], null, [], [], null, string.Empty,
LegacyOperatorStatusKind.Neutral, Overseas: workflow.Current);
using var document = JsonDocument.Parse(LegacyBridgeProtocol.SerializeState(snapshot));
var overseas = document.RootElement.GetProperty("payload").GetProperty("overseas");
var action = overseas.GetProperty("actions").EnumerateArray().Single(value =>
value.GetProperty("actionId").GetString() == "stock-current");
Assert.False(action.GetProperty("isAvailable").GetBoolean());
Assert.Equal("선택한 해외종목의 국가 코드는 두 글자여야 합니다.",
action.GetProperty("reason").GetString());
Assert.DoesNotContain("GBR", document.RootElement.GetRawText(), StringComparison.Ordinal);
Assert.DoesNotContain("LNS@UNSUPPORTED", document.RootElement.GetRawText(), StringComparison.Ordinal);
}
[Fact]
public async Task ProjectionReportsRowsIsolatedByTheReadBoundary()
{
var workflow = new LegacyOverseasSelectionWorkflow(
new StaticIndustryService(),
new StaticStockService(isolatedInvalidRowCount: 16));
await workflow.SearchStocksAsync(string.Empty);
var snapshot = new LegacyOperatorSnapshot(
7, string.Empty, [], null, [], [], null, string.Empty,
LegacyOperatorStatusKind.Neutral, Overseas: workflow.Current);
using var document = JsonDocument.Parse(LegacyBridgeProtocol.SerializeState(snapshot));
var stock = document.RootElement.GetProperty("payload")
.GetProperty("overseas").GetProperty("stock");
Assert.Equal(16, stock.GetProperty("isolatedInvalidRowCount").GetInt32());
Assert.True(stock.GetProperty("isTruncated").GetBoolean());
Assert.Single(stock.GetProperty("results").EnumerateArray());
}
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(
IReadOnlyList<WorldStockSearchItem>? items = null,
int isolatedInvalidRowCount = 0) : IOverseasStockSearchService
{
public Task<WorldStockSearchResult> SearchAsync(
string query,
int maximumResults = LegacyOverseasStockSearchService.DefaultMaximumResults,
CancellationToken cancellationToken = default) =>
Task.FromResult(new WorldStockSearchResult(
query,
DateTimeOffset.Parse("2026-07-15T10:00:00+09:00"),
items ?? [new WorldStockSearchItem("Apple", "NAS@AAPL", "US")],
IsTruncated: isolatedInvalidRowCount > 0,
IsolatedInvalidRowCount: isolatedInvalidRowCount));
}
}