Files

353 lines
15 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"));
}
[Fact]
public async Task Search_AcceptsConservativeTruncationAfterInitialFiltering()
{
var service = new DelegateExpertService
{
Search = (query, _, _) => Task.FromResult(
SearchResult(query, Alpha) with { IsTruncated = true })
};
var controller = new LegacyExpertWorkflowController(service);
var searched = await controller.SearchAsync("ㄱㅈㅁ");
Assert.Equal(Alpha.ExpertName, Assert.Single(searched.Search.Results).ExpertName);
Assert.True(searched.Search.IsTruncated);
}
[Fact]
public async Task Search_StillRejectsImpossiblePartialTruncationForOrdinaryText()
{
var service = new DelegateExpertService
{
Search = (query, _, _) => Task.FromResult(
SearchResult(query, Alpha) with { IsTruncated = true })
};
var controller = new LegacyExpertWorkflowController(service);
await Assert.ThrowsAsync<ExpertSelectionDataException>(
() => controller.SearchAsync("Expert"));
}
[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_PreservesOriginalOneOverZeroCut()
{
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.True(Assert.Single(selected.Actions).IsAvailable);
var draft = controller.MaterializeSelectedAction(
LegacyExpertWorkflowController.ActionId);
Assert.Equal(0, draft.ItemCount);
Assert.Equal(0, draft.PageCount);
Assert.Equal("1/0", draft.PageText);
}
[Fact]
public async Task NullBuyAmount_RemainsVisibleAndCanBeComposedForFreshPrepareValidation()
{
var service = new DelegateExpertService
{
Search = (query, _, _) => Task.FromResult(SearchResult(query, Alpha)),
Preview = (identity, _, _) => Task.FromResult(PreviewResult(identity,
[
new ExpertRecommendationPreview(0, "005930", "Samsung", null),
new ExpertRecommendationPreview(1, "035720", "Kakao", 42_000m)
]))
};
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.Equal(2, selected.Preview.Items.Count);
Assert.Null(selected.Preview.Items[0].BuyAmount);
Assert.True(Assert.Single(selected.Actions).IsAvailable);
var draft = controller.MaterializeSelectedAction(
LegacyExpertWorkflowController.ActionId);
Assert.Equal(2, draft.ItemCount);
Assert.Equal(1, draft.PageCount);
Assert.Equal("1/1", draft.PageText);
Assert.Equal("0001", draft.ExpertCode);
Assert.Equal(Alpha.ExpertName, draft.ExpertName);
}
[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);
}
}
}