Files
MBN_STOCK_WEBVIEW/tests/MBN_STOCK_WEBVIEW.LegacyApplication.Tests/LegacyIndustrySelectionWorkflowTests.cs

348 lines
14 KiB
C#

using MBN_STOCK_WEBVIEW.LegacyApplication;
using MMoneyCoderSharp.Data;
namespace MBN_STOCK_WEBVIEW.LegacyApplication.Tests;
public sealed class LegacyIndustrySelectionWorkflowTests
{
[Fact]
public async Task LoadMarketUsesServiceOrderAndCopiesRowsIntoImmutableSnapshot()
{
var source = KospiRows().ToList();
var service = new QueueIndustryService(source);
var workflow = new LegacyIndustrySelectionWorkflow(service);
var state = await workflow.LoadMarketAsync(IndustryMarket.Kospi);
source.Clear();
Assert.Equal([IndustryMarket.Kospi], service.Markets);
Assert.Equal(3, state.Industries.Count);
Assert.Equal("전기전자", state.Industries[0].IndustryName);
Assert.Equal(22, state.Actions.Count);
Assert.Null(state.SelectedIndustry);
Assert.Null(state.FirstIndustry);
Assert.Null(state.SecondIndustry);
var rows = Assert.IsAssignableFrom<IList<IndustrySelection>>(state.Industries);
Assert.Throws<NotSupportedException>(() => rows.RemoveAt(0));
var actions = Assert.IsAssignableFrom<IList<LegacyIndustryActionDefinition>>(state.Actions);
Assert.Throws<NotSupportedException>(() => actions.RemoveAt(0));
}
[Fact]
public async Task ListDoubleClickRotatesFirstIntoSecondBeforeAssigningNewFirst()
{
var workflow = await KospiWorkflowAsync();
var selectedOnly = workflow.SelectIndustry(1);
Assert.Equal("운수장비", selectedOnly.SelectedIndustry?.IndustryName);
Assert.Null(selectedOnly.FirstIndustry);
var first = workflow.DoubleClickIndustry(0);
Assert.Equal("전기전자", first.SelectedIndustry?.IndustryName);
Assert.Equal("전기전자", first.FirstIndustry?.IndustryName);
Assert.Null(first.SecondIndustry);
var second = workflow.DoubleClickIndustry(2);
Assert.Equal("화학", second.SelectedIndustry?.IndustryName);
Assert.Equal("화학", second.FirstIndustry?.IndustryName);
Assert.Equal("전기전자", second.SecondIndustry?.IndustryName);
var third = workflow.DoubleClickIndustry(1);
Assert.Equal("운수장비", third.FirstIndustry?.IndustryName);
Assert.Equal("화학", third.SecondIndustry?.IndustryName);
}
[Fact]
public async Task SwapMatchesUc2EvenWhenOnlyOneTextSlotHasAValue()
{
var workflow = await KospiWorkflowAsync();
workflow.DoubleClickIndustry(0);
var swapped = workflow.SwapIndustries();
Assert.Null(swapped.FirstIndustry);
Assert.Equal("전기전자", swapped.SecondIndustry?.IndustryName);
var restored = workflow.SwapIndustries();
Assert.Equal("전기전자", restored.FirstIndustry?.IndustryName);
Assert.Null(restored.SecondIndustry);
}
[Fact]
public async Task SameMarketRefreshReconcilesIdentityButMarketChangeClearsSelection()
{
var initial = KospiRows();
var refreshed = new[] { initial[1], initial[0], initial[2] };
var kosdaq = KosdaqRows();
var service = new QueueIndustryService(initial, refreshed, kosdaq);
var workflow = new LegacyIndustrySelectionWorkflow(service);
await workflow.LoadMarketAsync(IndustryMarket.Kospi);
workflow.DoubleClickIndustry(0);
workflow.DoubleClickIndustry(1);
var reconciled = await workflow.LoadMarketAsync(IndustryMarket.Kospi);
Assert.Equal(0, reconciled.SelectedIndex);
Assert.Equal("운수장비", reconciled.SelectedIndustry?.IndustryName);
Assert.Equal("운수장비", reconciled.FirstIndustry?.IndustryName);
Assert.Equal("전기전자", reconciled.SecondIndustry?.IndustryName);
var changed = await workflow.LoadMarketAsync(IndustryMarket.Kosdaq);
Assert.Equal(IndustryMarket.Kosdaq, changed.Market);
Assert.Null(changed.SelectedIndex);
Assert.Null(changed.SelectedIndustry);
Assert.Null(changed.FirstIndustry);
Assert.Null(changed.SecondIndustry);
}
[Fact]
public async Task InvalidIndexAndEmptyClearAreNoOps()
{
var workflow = await KospiWorkflowAsync();
var before = workflow.Current;
Assert.Equal(before.Revision, workflow.SelectIndustry(-1).Revision);
Assert.Equal(before.Revision, workflow.DoubleClickIndustry(3).Revision);
Assert.Equal(before.Revision, workflow.ClearSelections().Revision);
Assert.Equal(before.Revision, workflow.SwapIndustries().Revision);
}
[Fact]
public async Task RepresentativeActionsMaterializeOnlyFromCatalogAndAuthoritativeState()
{
var workflow = await KospiWorkflowAsync(new FixedTimeProvider(
new DateTimeOffset(2026, 7, 11, 10, 0, 0, TimeSpan.Zero)));
workflow.DoubleClickIndustry(0);
workflow.DoubleClickIndustry(1);
var single = workflow.MaterializePlaylistDraft("one-column");
Assert.Equal("s5001", single.BuilderKey);
Assert.Equal("5001", single.SceneAlias);
Assert.Equal("운수장비", single.Title);
Assert.Equal(
new LegacyIndustrySceneSelectionDraft(
"INDUSTRY_KOSPI", "운수장비", "1열판", "CURRENT", string.Empty),
single.Selection);
Assert.True(single.IsEnabled);
Assert.Equal(6, single.FadeDuration);
var pair = workflow.MaterializePlaylistDraft("two-column");
Assert.Equal("s8018", pair.BuilderKey);
Assert.Equal("8018", pair.SceneAlias);
Assert.Equal("운수장비-전기전자", pair.Selection.Subject);
Assert.Equal("2열판", pair.Selection.GraphicType);
var yield = workflow.MaterializePlaylistDraft("yield-120일");
Assert.Equal("s5086", yield.BuilderKey);
Assert.Equal("5086", yield.SceneAlias);
Assert.Equal("OneHundredTwentyDays", yield.Selection.Subtype);
var candle = workflow.MaterializePlaylistDraft("candle-volume-240일");
Assert.Equal("s8010", candle.BuilderKey);
Assert.Equal("8056", candle.SceneAlias);
Assert.Equal("KOSPI_INDUSTRY", candle.Selection.GroupCode);
Assert.Equal("VOLUME", candle.Selection.Subtype);
var five = workflow.MaterializePlaylistDraft("five-row");
Assert.Equal("s5074", five.BuilderKey);
Assert.Equal("5074", five.SceneAlias);
Assert.Equal(5, five.PageSize);
Assert.Equal("PAGED_DOMESTIC_KOSPI", five.Selection.GroupCode);
Assert.Equal("INDEX", five.Selection.Subject);
Assert.Equal("INDUSTRY", five.Selection.Subtype);
Assert.Equal("2026-07-11", five.Selection.DataCode);
Assert.Equal(
"F21DE58003315EAB41F9FE7B5A573C71653056203E6743D05AA96E7FF1B8BF52",
five.RuntimeAssetSha256);
var square = workflow.MaterializePlaylistDraft("square");
Assert.Equal("8001", square.SceneAlias);
Assert.Equal("KOSPI", square.Selection.Subject);
Assert.Equal("네모그래프", square.Selection.GraphicType);
var sector = workflow.MaterializePlaylistDraft("sector");
Assert.Equal("5078", sector.SceneAlias);
Assert.Equal("SECTOR_INDEX", sector.Selection.GraphicType);
}
[Fact]
public async Task EveryKosdaqActionMaterializesWithItsExactAliasAndPageSize()
{
var service = new QueueIndustryService(KosdaqRows());
var workflow = new LegacyIndustrySelectionWorkflow(
service,
new FixedTimeProvider(new DateTimeOffset(2026, 7, 12, 0, 0, 0, TimeSpan.Zero)));
await workflow.LoadMarketAsync(IndustryMarket.Kosdaq);
workflow.DoubleClickIndustry(0);
workflow.DoubleClickIndustry(1);
foreach (var action in LegacyIndustryActionCatalog.GetActions(IndustryMarket.Kosdaq))
{
var draft = workflow.MaterializePlaylistDraft(action.ActionId);
Assert.Equal(action.ActionId, draft.ActionId);
Assert.Equal(action.BuilderKey, draft.BuilderKey);
Assert.Equal(action.SceneAlias, draft.SceneAlias);
Assert.Equal(action.PageSize, draft.PageSize);
Assert.Equal(IndustryMarket.Kosdaq, draft.Market);
}
Assert.Equal("8032", workflow.MaterializePlaylistDraft("two-column").SceneAlias);
Assert.Equal("8002", workflow.MaterializePlaylistDraft("square").SceneAlias);
Assert.Equal(
"PAGED_DOMESTIC_KOSDAQ",
workflow.MaterializePlaylistDraft("five-row").Selection.GroupCode);
}
[Fact]
public async Task SelectionPrerequisitesUnsafePairAndUnknownActionFailClosed()
{
var workflow = await KospiWorkflowAsync();
Assert.Throws<LegacyIndustryWorkflowException>(() =>
workflow.MaterializePlaylistDraft("one-column"));
Assert.Throws<LegacyIndustryWorkflowException>(() =>
workflow.MaterializePlaylistDraft("two-column"));
Assert.NotNull(workflow.MaterializePlaylistDraft("five-row"));
Assert.Throws<LegacyIndustryWorkflowException>(() =>
workflow.MaterializePlaylistDraft("../../5001"));
Assert.Throws<ArgumentNullException>(() =>
workflow.MaterializePlaylistDraft(null!));
var unsafePair = new LegacyIndustrySelectionWorkflow(new QueueIndustryService(
[
new IndustrySelection(IndustryMarket.Kospi, "전기-전자", "013"),
new IndustrySelection(IndustryMarket.Kospi, "운수장비", "015")
]));
await unsafePair.LoadMarketAsync(IndustryMarket.Kospi);
unsafePair.DoubleClickIndustry(0);
unsafePair.DoubleClickIndustry(1);
Assert.Throws<LegacyIndustryWorkflowException>(() =>
unsafePair.MaterializePlaylistDraft("two-column"));
}
[Fact]
public async Task InvalidServiceRowsFailAtomicallyBeforeStateChanges()
{
var service = new QueueIndustryService(
[
new IndustrySelection(IndustryMarket.Kosdaq, "반도체", "027")
]);
var workflow = new LegacyIndustrySelectionWorkflow(service);
await Assert.ThrowsAsync<LegacyIndustryWorkflowException>(() =>
workflow.LoadMarketAsync(IndustryMarket.Kospi));
Assert.Equal(0, workflow.Current.Revision);
Assert.Null(workflow.Current.Market);
Assert.Empty(workflow.Current.Industries);
}
[Fact]
public async Task SlowerPreviousMarketResponseCannotReplaceOrFaultNewerMarketState()
{
var service = new ControlledIndustryService();
var workflow = new LegacyIndustrySelectionWorkflow(service);
var older = workflow.LoadMarketAsync(IndustryMarket.Kospi);
var newer = workflow.LoadMarketAsync(IndustryMarket.Kosdaq);
service.Kosdaq.SetResult(KosdaqRows());
var newerState = await newer;
// This row is invalid for the older KOSPI request. Because that request is
// already stale, it must be ignored before it can disturb current state.
service.Kospi.SetResult(KosdaqRows());
var olderResult = await older;
Assert.Equal(IndustryMarket.Kosdaq, newerState.Market);
Assert.Equal(IndustryMarket.Kosdaq, olderResult.Market);
Assert.Equal(IndustryMarket.Kosdaq, workflow.Current.Market);
Assert.Equal("반도체", workflow.Current.Industries[0].IndustryName);
Assert.Equal(1, workflow.Current.Revision);
}
[Fact]
public async Task ClearSelectionsClearsHighlightedAndBothPairSlots()
{
var workflow = await KospiWorkflowAsync();
workflow.DoubleClickIndustry(0);
workflow.DoubleClickIndustry(1);
var cleared = workflow.ClearSelections();
Assert.Null(cleared.SelectedIndex);
Assert.Null(cleared.SelectedIndustry);
Assert.Null(cleared.FirstIndustry);
Assert.Null(cleared.SecondIndustry);
}
private static async Task<LegacyIndustrySelectionWorkflow> KospiWorkflowAsync(
TimeProvider? timeProvider = null)
{
var workflow = new LegacyIndustrySelectionWorkflow(
new QueueIndustryService(KospiRows()),
timeProvider);
await workflow.LoadMarketAsync(IndustryMarket.Kospi);
return workflow;
}
private static IndustrySelection[] KospiRows() =>
[
new(IndustryMarket.Kospi, "전기전자", "013"),
new(IndustryMarket.Kospi, "운수장비", "015"),
new(IndustryMarket.Kospi, "화학", "008")
];
private static IndustrySelection[] KosdaqRows() =>
[
new(IndustryMarket.Kosdaq, "반도체", "027"),
new(IndustryMarket.Kosdaq, "IT부품", "029")
];
private sealed class QueueIndustryService(
params IReadOnlyList<IndustrySelection>[] results) : IIndustrySelectionService
{
private readonly Queue<IReadOnlyList<IndustrySelection>> _results = new(results);
public List<IndustryMarket> Markets { get; } = [];
public Task<IReadOnlyList<IndustrySelection>> GetAsync(
IndustryMarket market,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
Markets.Add(market);
if (_results.Count == 0)
{
throw new InvalidOperationException("No fake industry result remains.");
}
return Task.FromResult(_results.Dequeue());
}
}
private sealed class FixedTimeProvider(DateTimeOffset utcNow) : TimeProvider
{
public override TimeZoneInfo LocalTimeZone => TimeZoneInfo.Utc;
public override DateTimeOffset GetUtcNow() => utcNow;
}
private sealed class ControlledIndustryService : IIndustrySelectionService
{
public TaskCompletionSource<IReadOnlyList<IndustrySelection>> Kospi { get; } =
new(TaskCreationOptions.RunContinuationsAsynchronously);
public TaskCompletionSource<IReadOnlyList<IndustrySelection>> Kosdaq { get; } =
new(TaskCreationOptions.RunContinuationsAsynchronously);
public Task<IReadOnlyList<IndustrySelection>> GetAsync(
IndustryMarket market,
CancellationToken cancellationToken = default) => market switch
{
IndustryMarket.Kospi => Kospi.Task.WaitAsync(cancellationToken),
IndustryMarket.Kosdaq => Kosdaq.Task.WaitAsync(cancellationToken),
_ => throw new ArgumentOutOfRangeException(nameof(market), market, null)
};
}
}