348 lines
14 KiB
C#
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)
|
|
};
|
|
}
|
|
}
|