289 lines
12 KiB
C#
289 lines
12 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|