feat: complete legacy UI and playout parity migration
This commit is contained in:
@@ -0,0 +1,288 @@
|
||||
using MMoneyCoderSharp.Data;
|
||||
|
||||
namespace MBN_STOCK_WEBVIEW.LegacyApplication.Tests;
|
||||
|
||||
public sealed class LegacyExpertWorkflowTests
|
||||
{
|
||||
private static readonly ExpertSelectionIdentity Alpha = new("0001", "강전문");
|
||||
private static readonly ExpertSelectionIdentity Beta = new("0002", "김전문");
|
||||
|
||||
[Fact]
|
||||
public async Task SearchSelectPreview_MaterializesClosedS5074DraftFromActionIdOnly()
|
||||
{
|
||||
var service = new DelegateExpertService
|
||||
{
|
||||
Search = (query, _, _) => Task.FromResult(SearchResult(query, Alpha, Beta)),
|
||||
Preview = (identity, _, _) => Task.FromResult(
|
||||
PreviewResult(identity, Recommendations(6)))
|
||||
};
|
||||
var controller = new LegacyExpertWorkflowController(service);
|
||||
|
||||
var search = await controller.SearchAsync(" 전문 ");
|
||||
var preview = await controller.SelectExpertAsync(search.Search.Results[1].SelectionId);
|
||||
var draft = controller.MaterializeSelectedAction("five-row-current");
|
||||
|
||||
Assert.Equal("전문", service.SearchQuery);
|
||||
Assert.Equal(500, service.SearchMaximum);
|
||||
Assert.Equal(100, service.PreviewMaximum);
|
||||
Assert.Equal("김전문", preview.SelectedExpert?.ExpertName);
|
||||
Assert.Equal(6, preview.Preview.Items.Count);
|
||||
Assert.Equal(2, preview.Preview.PageCount);
|
||||
Assert.True(Assert.Single(preview.Actions).IsAvailable);
|
||||
Assert.Equal("s5074", draft.BuilderKey);
|
||||
Assert.Equal("5074", draft.CutCode);
|
||||
Assert.Equal(new[] { "5074" }, draft.Aliases);
|
||||
Assert.Equal("PAGED_EXPERT", draft.Selection.GroupCode);
|
||||
Assert.Equal("김전문", draft.Selection.Subject);
|
||||
Assert.Equal("INPUT_ORDER", draft.Selection.GraphicType);
|
||||
Assert.Equal("CURRENT", draft.Selection.Subtype);
|
||||
Assert.Equal("0002", draft.Selection.DataCode);
|
||||
Assert.Equal(5, draft.PageSize);
|
||||
Assert.Equal(2, draft.PageCount);
|
||||
Assert.Equal(20, draft.MaximumPageCount);
|
||||
Assert.Equal("1/2", draft.PageText);
|
||||
Assert.Equal(draft.DraftId, draft.ToLegacyPlaylistEntry().EntryId);
|
||||
Assert.Equal("legacy-expert-workflow", draft.Source);
|
||||
|
||||
var method = typeof(LegacyExpertWorkflowController).GetMethod(
|
||||
nameof(LegacyExpertWorkflowController.MaterializeSelectedAction));
|
||||
Assert.NotNull(method);
|
||||
Assert.Collection(
|
||||
method.GetParameters(),
|
||||
parameter => Assert.Equal("actionId", parameter.Name));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(
|
||||
() => controller.MaterializeSelectedAction("5074-caller-override"));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(1, false, 1)]
|
||||
[InlineData(5, false, 1)]
|
||||
[InlineData(6, false, 2)]
|
||||
[InlineData(100, true, 20)]
|
||||
public async Task PageN_UsesFiveRowsAndNeverExceedsTwentyPages(
|
||||
int itemCount,
|
||||
bool truncated,
|
||||
int expectedPageCount)
|
||||
{
|
||||
var service = new DelegateExpertService
|
||||
{
|
||||
Search = (query, _, _) => Task.FromResult(SearchResult(query, Alpha)),
|
||||
Preview = (identity, _, _) => Task.FromResult(
|
||||
PreviewResult(identity, Recommendations(itemCount), truncated))
|
||||
};
|
||||
var controller = new LegacyExpertWorkflowController(service);
|
||||
var search = await controller.SearchAsync(string.Empty);
|
||||
var selected = await controller.SelectExpertAsync(
|
||||
search.Search.Results[0].SelectionId);
|
||||
|
||||
var draft = controller.MaterializeSelectedAction(
|
||||
LegacyExpertWorkflowController.ActionId);
|
||||
|
||||
Assert.Equal(expectedPageCount, selected.Preview.PageCount);
|
||||
Assert.Equal(expectedPageCount, draft.PageCount);
|
||||
Assert.InRange(draft.PageCount, 1, 20);
|
||||
Assert.Equal(truncated, draft.PreviewTruncated);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EmptyRecommendationPreview_RemainsVisibleButCannotCreateCut()
|
||||
{
|
||||
var service = new DelegateExpertService
|
||||
{
|
||||
Search = (query, _, _) => Task.FromResult(SearchResult(query, Alpha)),
|
||||
Preview = (identity, _, _) => Task.FromResult(
|
||||
PreviewResult(identity, []))
|
||||
};
|
||||
var controller = new LegacyExpertWorkflowController(service);
|
||||
var search = await controller.SearchAsync(string.Empty);
|
||||
|
||||
var selected = await controller.SelectExpertAsync(
|
||||
search.Search.Results[0].SelectionId);
|
||||
|
||||
Assert.Equal(LegacyExpertRequestStatus.Ready, selected.Preview.Status);
|
||||
Assert.Empty(selected.Preview.Items);
|
||||
Assert.Equal(0, selected.Preview.PageCount);
|
||||
Assert.False(Assert.Single(selected.Actions).IsAvailable);
|
||||
Assert.Throws<InvalidOperationException>(
|
||||
() => controller.MaterializeSelectedAction(
|
||||
LegacyExpertWorkflowController.ActionId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LateSearchResponse_CannotReplaceNewerExpertResult()
|
||||
{
|
||||
var first = new TaskCompletionSource<ExpertSearchResult>(
|
||||
TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var second = new TaskCompletionSource<ExpertSearchResult>(
|
||||
TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var service = new DelegateExpertService
|
||||
{
|
||||
Search = (query, _, _) => query == "첫째" ? first.Task : second.Task
|
||||
};
|
||||
var controller = new LegacyExpertWorkflowController(service);
|
||||
|
||||
var firstRequest = controller.SearchAsync("첫째");
|
||||
var secondRequest = controller.SearchAsync("둘째");
|
||||
second.SetResult(SearchResult("둘째", Beta));
|
||||
var newest = await secondRequest;
|
||||
first.SetResult(SearchResult("첫째", Alpha));
|
||||
var staleCompletion = await firstRequest;
|
||||
|
||||
Assert.Equal("둘째", newest.Search.Query);
|
||||
Assert.Equal("김전문", Assert.Single(newest.Search.Results).ExpertName);
|
||||
Assert.Equal("둘째", staleCompletion.Search.Query);
|
||||
Assert.Equal("김전문", Assert.Single(staleCompletion.Search.Results).ExpertName);
|
||||
Assert.Equal("둘째", controller.Current.Search.Query);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LatePreviewResponse_CannotReplaceNewerExpertSelection()
|
||||
{
|
||||
var alphaPreview = new TaskCompletionSource<ExpertPreviewResult>(
|
||||
TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var betaPreview = new TaskCompletionSource<ExpertPreviewResult>(
|
||||
TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var service = new DelegateExpertService
|
||||
{
|
||||
Search = (query, _, _) => Task.FromResult(SearchResult(query, Alpha, Beta)),
|
||||
Preview = (identity, _, _) => identity == Alpha
|
||||
? alphaPreview.Task
|
||||
: betaPreview.Task
|
||||
};
|
||||
var controller = new LegacyExpertWorkflowController(service);
|
||||
var search = await controller.SearchAsync(string.Empty);
|
||||
|
||||
var firstSelection = controller.SelectExpertAsync(search.Search.Results[0].SelectionId);
|
||||
var secondSelection = controller.SelectExpertAsync(search.Search.Results[1].SelectionId);
|
||||
betaPreview.SetResult(PreviewResult(Beta, Recommendations(2, "B")));
|
||||
var newest = await secondSelection;
|
||||
alphaPreview.SetResult(PreviewResult(Alpha, Recommendations(1, "A")));
|
||||
var staleCompletion = await firstSelection;
|
||||
|
||||
Assert.Equal("김전문", newest.SelectedExpert?.ExpertName);
|
||||
Assert.Equal(2, newest.Preview.Items.Count);
|
||||
Assert.Equal("김전문", staleCompletion.SelectedExpert?.ExpertName);
|
||||
Assert.Equal(2, staleCompletion.Preview.Items.Count);
|
||||
Assert.All(staleCompletion.Preview.Items, item =>
|
||||
Assert.StartsWith("B", item.StockCode, StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StrictExpertIdentityOrderAndPreviewIdentity_AreRevalidated()
|
||||
{
|
||||
var unsorted = new DelegateExpertService
|
||||
{
|
||||
Search = (query, _, _) => Task.FromResult(SearchResult(query, Beta, Alpha))
|
||||
};
|
||||
var unsortedController = new LegacyExpertWorkflowController(unsorted);
|
||||
await Assert.ThrowsAsync<ExpertSelectionDataException>(
|
||||
() => unsortedController.SearchAsync(string.Empty));
|
||||
Assert.Equal(
|
||||
LegacyExpertRequestStatus.Error,
|
||||
unsortedController.Current.Search.Status);
|
||||
|
||||
var wrongPreview = new DelegateExpertService
|
||||
{
|
||||
Search = (query, _, _) => Task.FromResult(SearchResult(query, Alpha)),
|
||||
Preview = (_, _, _) => Task.FromResult(
|
||||
PreviewResult(Beta, Recommendations(1)))
|
||||
};
|
||||
var wrongController = new LegacyExpertWorkflowController(wrongPreview);
|
||||
var search = await wrongController.SearchAsync(string.Empty);
|
||||
await Assert.ThrowsAsync<ExpertSelectionDataException>(
|
||||
() => wrongController.SelectExpertAsync(search.Search.Results[0].SelectionId));
|
||||
Assert.Equal(
|
||||
LegacyExpertRequestStatus.Error,
|
||||
wrongController.Current.Preview.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExpertSnapshots_AreImmutableAcrossLaterQueries()
|
||||
{
|
||||
var service = new DelegateExpertService
|
||||
{
|
||||
Search = (query, _, _) => Task.FromResult(
|
||||
query == "A" ? SearchResult(query, Alpha) : SearchResult(query, Beta))
|
||||
};
|
||||
var controller = new LegacyExpertWorkflowController(service);
|
||||
var before = await controller.SearchAsync("A");
|
||||
|
||||
var after = await controller.SearchAsync("B");
|
||||
|
||||
Assert.Equal("강전문", Assert.Single(before.Search.Results).ExpertName);
|
||||
Assert.Equal("김전문", Assert.Single(after.Search.Results).ExpertName);
|
||||
var rows = Assert.IsAssignableFrom<ICollection<LegacyExpertResultView>>(
|
||||
before.Search.Results);
|
||||
Assert.Throws<NotSupportedException>(() => rows.Clear());
|
||||
var actions = Assert.IsAssignableFrom<ICollection<LegacyExpertActionView>>(
|
||||
before.Actions);
|
||||
Assert.Throws<NotSupportedException>(() => actions.Add(before.Actions[0]));
|
||||
}
|
||||
|
||||
private static ExpertSearchResult SearchResult(
|
||||
string query,
|
||||
params ExpertSelectionIdentity[] identities) =>
|
||||
new(
|
||||
query,
|
||||
DateTimeOffset.Parse("2026-07-15T00:00:00+09:00"),
|
||||
identities.Select(identity =>
|
||||
new ExpertSelectorItem(identity, DataSourceKind.Oracle)).ToArray(),
|
||||
false);
|
||||
|
||||
private static ExpertPreviewResult PreviewResult(
|
||||
ExpertSelectionIdentity identity,
|
||||
IReadOnlyList<ExpertRecommendationPreview> items,
|
||||
bool truncated = false) =>
|
||||
new(
|
||||
identity,
|
||||
DateTimeOffset.Parse("2026-07-15T00:00:00+09:00"),
|
||||
items,
|
||||
truncated);
|
||||
|
||||
private static IReadOnlyList<ExpertRecommendationPreview> Recommendations(
|
||||
int count,
|
||||
string prefix = "S") =>
|
||||
Enumerable.Range(0, count)
|
||||
.Select(index => new ExpertRecommendationPreview(
|
||||
index,
|
||||
$"{prefix}{index:D4}",
|
||||
$"추천종목{prefix}{index:D4}",
|
||||
10_000m + index))
|
||||
.ToArray();
|
||||
|
||||
private sealed class DelegateExpertService : IExpertSelectionService
|
||||
{
|
||||
public Func<string, int, CancellationToken, Task<ExpertSearchResult>> Search { get; init; } =
|
||||
(query, _, _) => Task.FromResult(SearchResult(query));
|
||||
|
||||
public Func<ExpertSelectionIdentity, int, CancellationToken, Task<ExpertPreviewResult>>
|
||||
Preview
|
||||
{ get; init; } =
|
||||
(identity, _, _) => Task.FromResult(PreviewResult(identity, []));
|
||||
|
||||
public string? SearchQuery { get; private set; }
|
||||
|
||||
public int SearchMaximum { get; private set; }
|
||||
|
||||
public int PreviewMaximum { get; private set; }
|
||||
|
||||
public Task<ExpertSearchResult> SearchAsync(
|
||||
string query = "",
|
||||
int maximumResults = LegacyExpertSelectionService.DefaultMaximumResults,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
SearchQuery = query;
|
||||
SearchMaximum = maximumResults;
|
||||
return Search(query, maximumResults, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<ExpertPreviewResult> PreviewAsync(
|
||||
ExpertSelectionIdentity identity,
|
||||
int maximumItems = LegacyExpertSelectionService.DefaultMaximumPreviewItems,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
PreviewMaximum = maximumItems;
|
||||
return Preview(identity, maximumItems, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user