Files
MBN_STOCK_WEBVIEW/tests/MBN_STOCK_WEBVIEW.LegacyApplication.Tests/Comparison/LegacyOperatorComparisonIntegrationTests.cs

409 lines
16 KiB
C#

using MBN_STOCK_WEBVIEW.Core.Playout.Scenes;
using MMoneyCoderSharp.Data;
namespace MBN_STOCK_WEBVIEW.LegacyApplication.Tests;
public sealed class LegacyOperatorComparisonIntegrationTests
{
[Fact]
public async Task ComparisonTabBuildsPairAndAddsAuthoritativeSceneRow()
{
var data = new StaticComparisonDataService();
var store = new MemoryComparisonPairStore();
var controller = new LegacyOperatorController(
new EmptyStockLookup(),
comparisonWorkflow: new LegacyComparisonWorkflowController(data, store));
var selected = await controller.SelectTabAsync(LegacyOperatorTabId.Comparison);
Assert.NotNull(selected.Comparison);
Assert.Empty(selected.Comparison.SavedPairs);
var searched = await controller.SearchComparisonDomesticAsync("반도체");
var results = searched.Comparison!.DomesticSearch.Results;
controller.ChooseComparisonDomesticResult(results[0].SelectionId);
controller.ChooseComparisonDomesticResult(results[1].SelectionId);
var saved = await controller.AddComparisonPairAsync();
Assert.Single(saved.Comparison!.SavedPairs);
Assert.Single(store.Writes);
var added = controller.ActivateComparisonAction("comparison-candle");
var row = Assert.Single(added.Playlist);
Assert.Equal("5026", row.ToPlayoutEntry().CutCode);
Assert.Equal(
"KOSPI:000660|KOSPI:005930",
Assert.IsType<LegacySceneSelection>(row.ToPlayoutEntry().Selection).DataCode);
Assert.Equal("비교", row.MarketText);
}
[Fact]
public async Task ComparisonParentDoubleClickAddsOnlyDirectChildrenInOriginalOrder()
{
var controller = new LegacyOperatorController(
new EmptyStockLookup(),
comparisonWorkflow: new LegacyComparisonWorkflowController(
new StaticComparisonDataService(),
new MemoryComparisonPairStore()));
await controller.SelectTabAsync(LegacyOperatorTabId.Comparison);
var searched = await controller.SearchComparisonDomesticAsync("반도체");
controller.ChooseComparisonDomesticResult(
searched.Comparison!.DomesticSearch.Results[0].SelectionId);
controller.ChooseComparisonDomesticResult(
searched.Comparison.DomesticSearch.Results[1].SelectionId);
await controller.AddComparisonPairAsync();
var added = controller.ActivateComparisonSection(1);
Assert.Collection(
added.Playlist,
row => Assert.Equal("5026", row.SceneAlias),
row => Assert.Equal("5087", row.SceneAlias));
}
[Fact]
public async Task DeleteAllComparisonPairs_NoPreservesRowsAndPerformsNoWrite()
{
var (controller, store) = await CreateControllerWithSavedPairAsync();
var writesBeforeRequest = store.Writes.Count;
var requested = await controller.DeleteComparisonPairAsync(all: true);
Assert.Equal("모두 삭제하겠습니까?", requested.Dialog?.Message);
Assert.Equal("MmoneyCoder", requested.Dialog?.Caption);
Assert.True(requested.Dialog?.IsConfirmation);
Assert.Single(requested.Comparison!.SavedPairs);
Assert.Equal(writesBeforeRequest, store.Writes.Count);
var dismissed = controller.DismissDialog();
Assert.True(dismissed.Dialog?.IsConfirmation);
Assert.Equal(writesBeforeRequest, store.Writes.Count);
var canceled = controller.CancelDialog();
Assert.Null(canceled.Dialog);
Assert.Single(canceled.Comparison!.SavedPairs);
Assert.Equal(writesBeforeRequest, store.Writes.Count);
await controller.ConfirmDialogAsync();
Assert.Equal(writesBeforeRequest, store.Writes.Count);
}
[Fact]
public async Task DeleteAllComparisonPairs_YesConsumesConfirmationBeforeSingleWrite()
{
var (controller, store) = await CreateControllerWithSavedPairAsync();
var writesBeforeRequest = store.Writes.Count;
await controller.DeleteComparisonPairAsync(all: true);
var confirmed = await controller.ConfirmDialogAsync();
Assert.Null(confirmed.Dialog);
Assert.Empty(confirmed.Comparison!.SavedPairs);
Assert.Equal(writesBeforeRequest + 1, store.Writes.Count);
Assert.Empty(store.Writes[^1]);
await controller.ConfirmDialogAsync();
Assert.Equal(writesBeforeRequest + 1, store.Writes.Count);
}
[Fact]
public async Task DeleteAllComparisonPairs_ChangedListInvalidatesPendingConfirmation()
{
var (controller, store) = await CreateControllerWithSavedPairAsync();
var writesBeforeRequest = store.Writes.Count;
var requested = await controller.DeleteComparisonPairAsync(all: true);
var rowId = Assert.Single(requested.Comparison!.SavedPairs).RowId;
controller.SelectComparisonPair(rowId);
await controller.DeleteComparisonPairAsync(all: true);
var rejected = await controller.ConfirmDialogAsync();
Assert.Single(rejected.Comparison!.SavedPairs);
Assert.Equal(writesBeforeRequest, store.Writes.Count);
Assert.Equal(LegacyOperatorStatusKind.Error, rejected.StatusKind);
}
[Fact]
public async Task DeleteAllComparisonPairs_EmptyListStillPromptsAndYesDoesNotWrite()
{
var store = new MemoryComparisonPairStore();
var controller = new LegacyOperatorController(
new EmptyStockLookup(),
comparisonWorkflow: new LegacyComparisonWorkflowController(
new StaticComparisonDataService(),
store));
await controller.SelectTabAsync(LegacyOperatorTabId.Comparison);
var requested = await controller.DeleteComparisonPairAsync(all: true);
Assert.Equal("모두 삭제하겠습니까?", requested.Dialog?.Message);
Assert.True(requested.Dialog?.IsConfirmation);
var confirmed = await controller.ConfirmDialogAsync();
Assert.Empty(confirmed.Comparison!.SavedPairs);
Assert.Empty(store.Writes);
}
[Fact]
public async Task LegacyComparisonImport_requires_explicit_confirmation_and_consumes_yes_once()
{
var store = new MemoryComparisonPairStore();
var importer = new StaticComparisonImporter();
var controller = new LegacyOperatorController(
new EmptyStockLookup(),
comparisonWorkflow: new LegacyComparisonWorkflowController(
new StaticComparisonDataService(),
store,
pairImporter: importer));
await controller.SelectTabAsync(LegacyOperatorTabId.Comparison);
var requested = controller.RequestLegacyComparisonPairImport();
Assert.True(requested.Dialog?.IsConfirmation);
Assert.Contains("없는 항목만", requested.Dialog?.Message, StringComparison.Ordinal);
Assert.Empty(store.Writes);
Assert.Equal(0, importer.Calls);
var confirmed = await controller.ConfirmDialogAsync();
Assert.Null(confirmed.Dialog);
Assert.Single(confirmed.Comparison!.SavedPairs);
Assert.Equal(1, confirmed.Comparison.LastImport?.AddedPairCount);
Assert.Single(store.Writes);
Assert.Equal(1, importer.Calls);
await controller.ConfirmDialogAsync();
Assert.Single(store.Writes);
Assert.Equal(1, importer.Calls);
}
private static async Task<(LegacyOperatorController Controller,
MemoryComparisonPairStore Store)> CreateControllerWithSavedPairAsync()
{
var store = new MemoryComparisonPairStore();
var controller = new LegacyOperatorController(
new EmptyStockLookup(),
comparisonWorkflow: new LegacyComparisonWorkflowController(
new StaticComparisonDataService(),
store));
await controller.SelectTabAsync(LegacyOperatorTabId.Comparison);
var searched = await controller.SearchComparisonDomesticAsync("반도체");
controller.ChooseComparisonDomesticResult(
searched.Comparison!.DomesticSearch.Results[0].SelectionId);
controller.ChooseComparisonDomesticResult(
searched.Comparison.DomesticSearch.Results[1].SelectionId);
await controller.AddComparisonPairAsync();
return (controller, store);
}
private sealed class EmptyStockLookup : ILegacyStockLookup
{
public Task<LegacyStockSearchData> SearchAsync(
string query,
CancellationToken cancellationToken = default) => Task.FromResult(
new LegacyStockSearchData(query, [], []));
}
private sealed class StaticComparisonDataService : ILegacyComparisonDataService
{
private static readonly IReadOnlyList<LegacyComparisonTarget> Domestic =
[
LegacyComparisonTarget.CreateKrxStock(
LegacyComparisonDomesticMarket.Kospi,
"삼성전자",
"005930"),
LegacyComparisonTarget.CreateKrxStock(
LegacyComparisonDomesticMarket.Kospi,
"SK하이닉스",
"000660")
];
public Task<IReadOnlyList<LegacyComparisonTarget>> SearchDomesticAsync(
string query,
int maximumResults,
CancellationToken cancellationToken = default) => Task.FromResult(Domestic);
public Task<IReadOnlyList<LegacyComparisonTarget>> SearchWorldAsync(
string query,
int maximumResults,
CancellationToken cancellationToken = default) => Task.FromResult<
IReadOnlyList<LegacyComparisonTarget>>([]);
public Task<LegacyComparisonTarget?> ResolveStoredTargetAsync(
LegacyComparisonStoredTargetReference target,
CancellationToken cancellationToken = default) => Task.FromResult<
LegacyComparisonTarget?>(null);
}
private sealed class MemoryComparisonPairStore : ILegacyComparisonPairStore
{
public List<IReadOnlyList<LegacyComparisonPairWriteRow>> Writes { get; } = [];
public Task<byte[]?> ReadCp949Async(CancellationToken cancellationToken = default) =>
Task.FromResult<byte[]?>(null);
public Task WriteCp949Async(
IReadOnlyList<LegacyComparisonPairWriteRow> rows,
CancellationToken cancellationToken = default)
{
Writes.Add(rows.ToArray());
return Task.CompletedTask;
}
}
private sealed class StaticComparisonImporter : ILegacyComparisonPairImportService
{
public int Calls { get; private set; }
public Task<LegacyComparisonImportResult> ImportAsync(
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
Calls++;
return Task.FromResult(new LegacyComparisonImportResult(
new string('A', 64),
1,
DateTimeOffset.Parse("2026-07-22T12:00:00+09:00"),
[new LegacyComparisonImportPair(
1,
new LegacyComparisonImportTarget(
LegacyComparisonImportTargetKind.MarketTarget,
MarketTarget: "Kospi"),
new LegacyComparisonImportTarget(
LegacyComparisonImportTargetKind.MarketTarget,
MarketTarget: "Kosdaq"))]));
}
}
}
public sealed class LegacyComparisonProductionAdapterTests
{
[Fact]
public async Task DataAdapterMapsDomesticNxtAndWorldIdentities()
{
var lookup = new StaticStockLookup();
var world = new StaticWorldLookup();
var service = new LegacyComparisonDataService(lookup, world);
var domestic = await service.SearchDomesticAsync("삼성", 10);
Assert.Equal(LegacyComparisonTargetKind.KrxStock, domestic[0].Kind);
Assert.Equal(LegacyComparisonTargetKind.NxtStock, domestic[1].Kind);
Assert.Equal("삼성전자(NXT)", domestic[1].DisplayName);
var overseas = Assert.Single(await service.SearchWorldAsync("APPLE", 10));
Assert.Equal("world:US:AAPL", overseas.TargetId);
var resolved = await service.ResolveStoredTargetAsync(
new LegacyComparisonStoredTargetReference("APPLE", string.Empty));
Assert.Equal(overseas, resolved);
}
[Fact]
public async Task DataAdapter_preserves_world_row_isolation_diagnostics()
{
var world = new StaticWorldLookup
{
Result = new WorldStockSearchResult(
string.Empty,
DateTimeOffset.UtcNow,
[new WorldStockSearchItem("APPLE", "AAPL", "US")],
true,
16)
};
var service = new LegacyComparisonDataService(new StaticStockLookup(), world);
var result = await service.SearchWorldWithDiagnosticsAsync(string.Empty, 100);
Assert.Single(result.Items);
Assert.True(result.IsTruncated);
Assert.Equal(16, result.IsolatedInvalidRowCount);
}
[Fact]
public async Task DataAdapter_keeps_comma_name_visible_as_one_deferred_row()
{
var world = new StaticWorldLookup
{
Result = new WorldStockSearchResult(
string.Empty,
DateTimeOffset.UtcNow,
[new WorldStockSearchItem("ACME, INC", "ACME", "US")],
false)
};
var service = new LegacyComparisonDataService(new StaticStockLookup(), world);
var result = await service.SearchWorldWithDiagnosticsAsync(string.Empty, 100);
var target = Assert.Single(result.Items);
Assert.False(target.CanSelect);
Assert.Equal(1, result.DeferredUnsafeRowCount);
Assert.Equal(0, result.IsolatedInvalidRowCount);
}
[Fact]
public async Task FileStoreRoundTripsStrictCp949WithAtomicTarget()
{
var directory = Path.Combine(
Path.GetTempPath(),
$"mbn-comparison-{Guid.NewGuid():N}");
var path = Path.Combine(directory, "종목비교.dat");
try
{
var store = new LegacyComparisonPairFileStore(path);
await store.WriteCp949Async(
[
new LegacyComparisonPairWriteRow(
1,
"삼성전자",
"코스피",
"SK하이닉스",
"코스피")
]);
var bytes = Assert.IsType<byte[]>(await store.ReadCp949Async());
var parsed = Assert.Single(LegacyComparisonFileParser.Parse(bytes));
Assert.Equal("삼성전자", parsed.First.Name);
Assert.Empty(Directory.EnumerateFiles(directory, "*.tmp"));
}
finally
{
if (Directory.Exists(directory))
{
Directory.Delete(directory, recursive: true);
}
}
}
private sealed class StaticStockLookup : ILegacyStockLookup
{
public Task<LegacyStockSearchData> SearchAsync(
string query,
CancellationToken cancellationToken = default) => Task.FromResult(
new LegacyStockSearchData(
query,
["삼성전자", "삼성전자(NXT)"],
[
new LegacyStockIdentity(
LegacyDomesticStockMarket.Kospi,
"삼성전자",
"005930"),
new LegacyStockIdentity(
LegacyDomesticStockMarket.NxtKospi,
"삼성전자(NXT)",
"005930")
]));
}
private sealed class StaticWorldLookup : IWorldStockSearchService
{
public WorldStockSearchResult Result { get; init; } = new(
string.Empty,
DateTimeOffset.UtcNow,
[new WorldStockSearchItem("APPLE", "AAPL", "US")],
false);
public Task<WorldStockSearchResult> SearchAsync(
string query,
int maximumResults = LegacyWorldStockSearchService.DefaultMaximumResults,
CancellationToken cancellationToken = default) => Task.FromResult(
Result with { Query = query.Trim() });
}
}