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

236 lines
10 KiB
C#

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();
}
}