feat: advance legacy UI behavior parity
This commit is contained in:
@@ -125,7 +125,7 @@ public sealed class CandleSceneDataLoaderTests
|
||||
CandleChartMode.Price,
|
||||
"World",
|
||||
"NAS@WORLD",
|
||||
"DE"),
|
||||
"DEU"),
|
||||
Request(
|
||||
CandleCutAlias.Cut8061,
|
||||
CandleMarketTarget.OverseasIndex,
|
||||
@@ -246,7 +246,7 @@ public sealed class CandleSceneDataLoaderTests
|
||||
Assert.All(stockExecutor.Calls, call =>
|
||||
{
|
||||
Assert.Contains("a.f_fdtc = '1'", call.Spec.Sql, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains(
|
||||
Assert.DoesNotContain(
|
||||
"a.f_natc IN ('US', 'TW')",
|
||||
call.Spec.Sql,
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
@@ -268,7 +268,7 @@ public sealed class CandleSceneDataLoaderTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Overseas_stock_candle_preserves_the_original_taiwan_scope()
|
||||
public async Task Overseas_stock_candle_accepts_an_exact_all_nation_identity()
|
||||
{
|
||||
var executor = new RecordingExecutor(QuoteTable(), RowsTable(false));
|
||||
|
||||
@@ -277,18 +277,18 @@ public sealed class CandleSceneDataLoaderTests
|
||||
CandleCutAlias.Cut8061,
|
||||
CandleMarketTarget.OverseasStock,
|
||||
CandleChartMode.Price,
|
||||
"Taiwan Stock",
|
||||
"TWS@2330",
|
||||
"TW"));
|
||||
"London Stock",
|
||||
"LNS@LON",
|
||||
"GB"));
|
||||
|
||||
Assert.All(executor.Calls, call =>
|
||||
{
|
||||
Assert.Contains(
|
||||
Assert.DoesNotContain(
|
||||
"a.f_natc IN ('US', 'TW')",
|
||||
call.Spec.Sql,
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Equal(
|
||||
"TW",
|
||||
"GB",
|
||||
Assert.Single(
|
||||
call.Spec.Parameters,
|
||||
parameter => parameter.Name == "nationCode").Value);
|
||||
|
||||
@@ -84,16 +84,25 @@ public sealed class LegacyComparisonPairImportServiceTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parser_RejectsDuplicatePairsAndInvalidCp949()
|
||||
public void Parser_AllowsDuplicatePairsAsDistinctOrderedRows()
|
||||
{
|
||||
var duplicate = Encode(
|
||||
"1^삼성전자,기아^코스피^코스피^^^^^\r\n" +
|
||||
"2^삼성전자,기아^코스피^코스피^^^^^\r\n");
|
||||
Assert.Equal(
|
||||
LegacyComparisonImportFailure.SourceInvalid,
|
||||
Assert.Throws<LegacyComparisonImportException>(
|
||||
() => LegacyComparisonFileParser.Parse(duplicate)).Failure);
|
||||
|
||||
var rows = LegacyComparisonFileParser.Parse(duplicate);
|
||||
|
||||
Assert.Collection(
|
||||
rows,
|
||||
row => Assert.Equal(1, row.RowNumber),
|
||||
row => Assert.Equal(2, row.RowNumber));
|
||||
Assert.Equal(rows[0].First, rows[1].First);
|
||||
Assert.Equal(rows[0].Second, rows[1].Second);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parser_RejectsInvalidCp949()
|
||||
{
|
||||
Assert.Equal(
|
||||
LegacyComparisonImportFailure.SourceInvalid,
|
||||
Assert.Throws<LegacyComparisonImportException>(
|
||||
@@ -228,6 +237,32 @@ public sealed class LegacyComparisonPairImportServiceTests
|
||||
Assert.Equal("엔비디아", executor.Calls[3].Spec.Parameters[0].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ImportAsync_PreservesDuplicatePairsAsDistinctStableRows()
|
||||
{
|
||||
var bytes = Encode(
|
||||
"1^삼성전자,기아^코스피^코스피^^^^^\r\n" +
|
||||
"2^삼성전자,기아^코스피^코스피^^^^^\r\n");
|
||||
var executor = new RecordingExecutor(call =>
|
||||
string.Equals(
|
||||
call.Spec.Parameters[0].Value as string,
|
||||
"삼성전자",
|
||||
StringComparison.Ordinal)
|
||||
? StockTable("삼성전자", "005930")
|
||||
: StockTable("기아", "000270"));
|
||||
var service = new LegacyComparisonPairImportService(
|
||||
executor,
|
||||
new MemorySource(bytes));
|
||||
|
||||
var result = await service.ImportAsync();
|
||||
|
||||
Assert.Equal(2, result.SourceRowCount);
|
||||
Assert.Equal([1, 2], result.Pairs.Select(static pair => pair.RowNumber));
|
||||
Assert.Equal(result.Pairs[0].First, result.Pairs[1].First);
|
||||
Assert.Equal(result.Pairs[0].Second, result.Pairs[1].Second);
|
||||
Assert.Equal(2, executor.Calls.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ImportAsync_MapsAllTwentyFourOriginalFixedTargetsExactly()
|
||||
{
|
||||
|
||||
@@ -69,7 +69,7 @@ public sealed class LegacyExpertCatalogPersistenceServiceTests
|
||||
var parent = transaction.Commands[0];
|
||||
Assert.Contains("NOT EXISTS", parent.Sql, StringComparison.Ordinal);
|
||||
Assert.Equal("홍'길동", Parameter(parent, "expert_name").Value);
|
||||
Assert.Equal("홍'길동", Parameter(parent, "conflict_name").Value);
|
||||
Assert.DoesNotContain(parent.Parameters, parameter => parameter.Name == "conflict_name");
|
||||
var first = transaction.Commands[1];
|
||||
Assert.Equal("005930", Parameter(first, "stock_code").Value);
|
||||
Assert.Equal(72000m, Parameter(first, "buy_amount").Value);
|
||||
@@ -100,8 +100,8 @@ public sealed class LegacyExpertCatalogPersistenceServiceTests
|
||||
var update = transaction.Commands[0];
|
||||
Assert.Equal("기존 전문가", Parameter(update, "expected_name").Value);
|
||||
Assert.Equal("새 전문가", Parameter(update, "new_name").Value);
|
||||
Assert.Equal("새 전문가", Parameter(update, "duplicate_name").Value);
|
||||
Assert.Contains("NOT EXISTS", update.Sql, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain(update.Parameters, parameter => parameter.Name == "duplicate_name");
|
||||
Assert.DoesNotContain("NOT EXISTS", update.Sql, StringComparison.Ordinal);
|
||||
Assert.Equal(
|
||||
OperatorCatalogAffectedRowsRule.ExactlyOne,
|
||||
update.AffectedRowsRule);
|
||||
@@ -132,26 +132,21 @@ public sealed class LegacyExpertCatalogPersistenceServiceTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Mutations_RejectDuplicateNamesCodesOrderAndUnsafeAmountsBeforeExecutor()
|
||||
public async Task Mutations_AllowRepeatedRecommendationsButRejectOrderAndUnsafeAmountsBeforeExecutor()
|
||||
{
|
||||
var mutation = new CatalogRecordingMutationExecutor();
|
||||
var service = new LegacyExpertCatalogPersistenceService(UnusedQuery(), mutation);
|
||||
var identity = new ExpertSelectionIdentity("0001", "전문가");
|
||||
|
||||
await Assert.ThrowsAsync<ArgumentException>(() => service.CreateAsync(
|
||||
await service.CreateAsync(
|
||||
new ExpertCatalogDefinition(
|
||||
identity,
|
||||
[
|
||||
new ExpertCatalogRecommendation(0, "005930", "삼성전자", 1m),
|
||||
new ExpertCatalogRecommendation(1, "005930", "다른이름", 2m)
|
||||
])));
|
||||
await Assert.ThrowsAsync<ArgumentException>(() => service.CreateAsync(
|
||||
new ExpertCatalogDefinition(
|
||||
identity,
|
||||
[
|
||||
new ExpertCatalogRecommendation(0, "005930", "삼성전자", 1m),
|
||||
new ExpertCatalogRecommendation(1, "035720", "삼성전자", 2m)
|
||||
])));
|
||||
new ExpertCatalogRecommendation(1, "005930", "삼성전자", 2m)
|
||||
]));
|
||||
Assert.Equal(3, Assert.Single(mutation.Calls).Transaction.Commands.Count);
|
||||
mutation.Calls.Clear();
|
||||
await Assert.ThrowsAsync<ArgumentException>(() => service.CreateAsync(
|
||||
new ExpertCatalogDefinition(
|
||||
identity,
|
||||
|
||||
@@ -133,9 +133,8 @@ public sealed class LegacyExpertSelectionServiceTests
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Analyst", "0001", "Analyst", "0002")]
|
||||
[InlineData("Analyst", "0001", "Other", "0001")]
|
||||
public async Task SearchAsync_RejectsDuplicateExpertNameOrCode(
|
||||
public async Task SearchAsync_RejectsDuplicateExpertCode(
|
||||
string name1,
|
||||
string code1,
|
||||
string name2,
|
||||
@@ -149,7 +148,19 @@ public sealed class LegacyExpertSelectionServiceTests
|
||||
var exception = await Assert.ThrowsAsync<ExpertSelectionDataException>(
|
||||
() => service.SearchAsync());
|
||||
|
||||
Assert.Contains("duplicate expert name or code", exception.Message, StringComparison.Ordinal);
|
||||
Assert.Contains("duplicate expert code", exception.Message, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SearchAsync_AllowsDuplicateExpertNamesWithDistinctCodes()
|
||||
{
|
||||
var executor = new RecordingExecutor(_ => SearchTable(
|
||||
("Analyst", "0001"),
|
||||
("Analyst", "0002")));
|
||||
|
||||
var result = await new LegacyExpertSelectionService(executor).SearchAsync();
|
||||
|
||||
Assert.Equal(2, result.Items.Count);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@@ -351,11 +362,9 @@ public sealed class LegacyExpertSelectionServiceTests
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("005930", "Samsung", "0", "005930", "Other", "1")]
|
||||
[InlineData("005930", "Samsung", "0", "035720", "Samsung", "1")]
|
||||
[InlineData("005930", "Samsung", "0", "035720", "Kakao", "0")]
|
||||
[InlineData("005930", "Samsung", "2", "035720", "Kakao", "1")]
|
||||
public async Task PreviewAsync_RejectsDuplicateOrNonIncreasingRecommendationIdentity(
|
||||
public async Task PreviewAsync_RejectsDuplicateOrNonIncreasingPlayIndex(
|
||||
string code1,
|
||||
string name1,
|
||||
string index1,
|
||||
@@ -371,6 +380,18 @@ public sealed class LegacyExpertSelectionServiceTests
|
||||
new LegacyExpertSelectionService(executor).PreviewAsync(Identity()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PreviewAsync_AllowsRepeatedStockCodeAndNameAtDistinctPlayIndexes()
|
||||
{
|
||||
var executor = new RecordingExecutor(_ => PreviewTable(
|
||||
("Analyst A", "0001", "005930", "Samsung", "100", "0"),
|
||||
("Analyst A", "0001", "005930", "Samsung", "200", "1")));
|
||||
|
||||
var result = await new LegacyExpertSelectionService(executor).PreviewAsync(Identity());
|
||||
|
||||
Assert.Equal(2, result.Items.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PreviewAsync_StopsAfterProviderCancellation()
|
||||
{
|
||||
|
||||
@@ -32,7 +32,7 @@ public sealed class LegacyIndustrySelectionServiceTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAsync_rejects_wrong_schema_duplicate_unsafe_code_and_control_text()
|
||||
public async Task GetAsync_rejects_wrong_schema_unsafe_code_and_control_text()
|
||||
{
|
||||
var wrongSchema = Table(("반도체", "013"));
|
||||
wrongSchema.Columns[0].ColumnName = "NAME";
|
||||
@@ -40,11 +40,6 @@ public sealed class LegacyIndustrySelectionServiceTests
|
||||
new LegacyIndustrySelectionService(new RecordingExecutor(wrongSchema))
|
||||
.GetAsync(IndustryMarket.Kospi));
|
||||
|
||||
await Assert.ThrowsAsync<IndustrySelectionDataException>(() =>
|
||||
new LegacyIndustrySelectionService(new RecordingExecutor(
|
||||
Table(("반도체", "013"), ("반도체", "013"))))
|
||||
.GetAsync(IndustryMarket.Kospi));
|
||||
|
||||
await Assert.ThrowsAsync<IndustrySelectionDataException>(() =>
|
||||
new LegacyIndustrySelectionService(new RecordingExecutor(Table(("반도체", "01-3"))))
|
||||
.GetAsync(IndustryMarket.Kospi));
|
||||
@@ -55,17 +50,24 @@ public sealed class LegacyIndustrySelectionServiceTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAsync_rejects_ambiguous_duplicate_name_or_code()
|
||||
public async Task GetAsync_preserves_duplicate_name_and_code_rows_in_original_order()
|
||||
{
|
||||
await Assert.ThrowsAsync<IndustrySelectionDataException>(() =>
|
||||
new LegacyIndustrySelectionService(new RecordingExecutor(
|
||||
Table(("반도체", "013"), ("반도체", "014"))))
|
||||
.GetAsync(IndustryMarket.Kospi));
|
||||
var result = await new LegacyIndustrySelectionService(new RecordingExecutor(
|
||||
Table(
|
||||
("반도체", "013"),
|
||||
("반도체", "014"),
|
||||
("운송", "013"),
|
||||
("반도체", "013"))))
|
||||
.GetAsync(IndustryMarket.Kospi);
|
||||
|
||||
await Assert.ThrowsAsync<IndustrySelectionDataException>(() =>
|
||||
new LegacyIndustrySelectionService(new RecordingExecutor(
|
||||
Table(("반도체", "013"), ("운송", "013"))))
|
||||
.GetAsync(IndustryMarket.Kosdaq));
|
||||
Assert.Equal(
|
||||
[
|
||||
new IndustrySelection(IndustryMarket.Kospi, "반도체", "013"),
|
||||
new IndustrySelection(IndustryMarket.Kospi, "반도체", "014"),
|
||||
new IndustrySelection(IndustryMarket.Kospi, "운송", "013"),
|
||||
new IndustrySelection(IndustryMarket.Kospi, "반도체", "013")
|
||||
],
|
||||
result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
using System.Data;
|
||||
using MMoneyCoderSharp.Data;
|
||||
|
||||
namespace MBN_STOCK_WEBVIEW.Core.Tests;
|
||||
|
||||
public sealed class LegacyOverseasStockSearchServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task BlankQuery_UsesOriginalUsTwInitialScopeAndOrder()
|
||||
{
|
||||
var executor = new RecordingExecutor(WorldStockTable(("Apple", "NAS@AAPL", "US")));
|
||||
var result = await new LegacyOverseasStockSearchService(executor).SearchAsync(" ");
|
||||
|
||||
Assert.Equal(string.Empty, result.Query);
|
||||
Assert.Equal(new WorldStockSearchItem("Apple", "NAS@AAPL", "US"), Assert.Single(result.Items));
|
||||
var call = Assert.Single(executor.Calls);
|
||||
Assert.Equal("OVERSEAS_STOCK_SEARCH", call.Name);
|
||||
Assert.Contains("F_NATC IN ('US', 'TW')", call.Spec.Sql, StringComparison.Ordinal);
|
||||
Assert.Contains("ORDER BY F_INPUT_NAME", call.Spec.Sql, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("ORDER BY F_INPUT_NAME, F_SYMB", call.Spec.Sql, StringComparison.Ordinal);
|
||||
Assert.Single(call.Spec.Parameters);
|
||||
Assert.Equal("row_limit", call.Spec.Parameters[0].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NonblankQuery_UsesOriginalAllNationScopeWithBoundEscapedTerm()
|
||||
{
|
||||
var executor = new RecordingExecutor(WorldStockTable(("London Stock", "LNS@LON", "GB")));
|
||||
var result = await new LegacyOverseasStockSearchService(executor).SearchAsync(" lo_% ");
|
||||
|
||||
Assert.Equal("lo_%", result.Query);
|
||||
Assert.Equal("GB", Assert.Single(result.Items).NationCode);
|
||||
var call = Assert.Single(executor.Calls);
|
||||
Assert.DoesNotContain("F_NATC IN", call.Spec.Sql, StringComparison.Ordinal);
|
||||
Assert.Contains("LIKE '%' || :search_term || '%' ESCAPE '!'", call.Spec.Sql, StringComparison.Ordinal);
|
||||
Assert.Equal("LO!_!%", call.Spec.Parameters[0].Value);
|
||||
Assert.Equal("row_limit", call.Spec.Parameters[1].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AllowsDuplicateInputNamesWhenTheExactIdentityDiffersButRejectsDuplicateIdentity()
|
||||
{
|
||||
var ambiguous = new RecordingExecutor(WorldStockTable(
|
||||
("Alpha", "AAA", "US"),
|
||||
("Alpha", "AAA", "GB")));
|
||||
var result = await new LegacyOverseasStockSearchService(ambiguous).SearchAsync("alpha");
|
||||
Assert.Equal(2, result.Items.Count);
|
||||
Assert.Equal(["US", "GB"], result.Items.Select(item => item.NationCode));
|
||||
|
||||
var duplicate = new RecordingExecutor(WorldStockTable(
|
||||
("Alpha", "AAA", "US"),
|
||||
("Alpha", "AAA", "US")));
|
||||
await Assert.ThrowsAsync<WorldStockSearchDataException>(
|
||||
() => new LegacyOverseasStockSearchService(duplicate).SearchAsync("alpha"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DefaultQueryUsesTheFullBoundAndReportsWhenOriginalUnboundedResultsAreTruncated()
|
||||
{
|
||||
var rows = Enumerable.Range(0, LegacyOverseasStockSearchService.MaximumResults + 1)
|
||||
.Select(index => ($"Stock {index:D3}", $"LNS@{index:D4}", "GB"))
|
||||
.ToArray();
|
||||
var executor = new RecordingExecutor(WorldStockTable(rows));
|
||||
|
||||
var result = await new LegacyOverseasStockSearchService(executor).SearchAsync("stock");
|
||||
|
||||
Assert.Equal(LegacyOverseasStockSearchService.MaximumResults, result.Items.Count);
|
||||
Assert.True(result.IsTruncated);
|
||||
var rowLimit = Assert.Single(Assert.Single(executor.Calls).Spec.Parameters,
|
||||
parameter => parameter.Name == "row_limit");
|
||||
Assert.Equal(LegacyOverseasStockSearchService.MaximumResults + 1, rowLimit.Value);
|
||||
}
|
||||
|
||||
private static DataTable WorldStockTable(params (string Name, string Symbol, string Nation)[] rows)
|
||||
{
|
||||
var table = new DataTable();
|
||||
table.Columns.Add("F_INPUT_NAME", typeof(string));
|
||||
table.Columns.Add("F_SYMB", typeof(string));
|
||||
table.Columns.Add("F_NATC", typeof(string));
|
||||
foreach (var row in rows) table.Rows.Add(row.Name, row.Symbol, row.Nation);
|
||||
return table;
|
||||
}
|
||||
|
||||
private sealed record Call(string Name, DataQuerySpec Spec);
|
||||
|
||||
private sealed class RecordingExecutor(DataTable result) : IDataQueryExecutor
|
||||
{
|
||||
public List<Call> Calls { get; } = [];
|
||||
|
||||
public DataTable Execute(DataSourceKind source, string queryName, string query) =>
|
||||
throw new InvalidOperationException("String SQL fallback is forbidden.");
|
||||
|
||||
public Task<DataTable> ExecuteAsync(
|
||||
DataSourceKind source,
|
||||
string queryName,
|
||||
string query,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
throw new InvalidOperationException("String SQL fallback is forbidden.");
|
||||
|
||||
public Task<DataTable> ExecuteAsync(
|
||||
DataSourceKind source,
|
||||
string queryName,
|
||||
DataQuerySpec spec,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Assert.Equal(DataSourceKind.Oracle, source);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
Calls.Add(new Call(queryName, spec));
|
||||
return Task.FromResult(result.Copy());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,36 @@ public sealed class LegacyParameterizedSceneRequestResolverTests
|
||||
Assert.Equal(S5001NxtBadge.PreMarket, stock.Badge);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void S5001_foreign_stock_uses_exact_hidden_identity_when_present_and_keeps_legacy_rows_compatible()
|
||||
{
|
||||
var resolver = new LegacyParameterizedSceneRequestResolver(new RecordingExecutor());
|
||||
var exact = resolver.CreateS5001Request(new LegacyPlaylistEntry(
|
||||
"exact",
|
||||
"5001",
|
||||
Selection: new LegacySceneSelection(
|
||||
"FOREIGN_STOCK",
|
||||
"Shared Name",
|
||||
"1열판기본",
|
||||
"CURRENT",
|
||||
"LNS@LON|GB|1")));
|
||||
var legacy = resolver.CreateS5001Request(new LegacyPlaylistEntry(
|
||||
"legacy",
|
||||
"5001",
|
||||
Selection: new LegacySceneSelection(
|
||||
"FOREIGN_STOCK",
|
||||
"Legacy Name",
|
||||
"1열판기본",
|
||||
"CURRENT",
|
||||
string.Empty)));
|
||||
|
||||
var exactRequest = Assert.IsType<S5001ExactForeignStockLoadRequest>(exact);
|
||||
Assert.Equal("Shared Name", exactRequest.StockName);
|
||||
Assert.Equal("LNS@LON", exactRequest.ForeignSymbol);
|
||||
Assert.Equal("GB", exactRequest.ForeignNationCode);
|
||||
Assert.Equal("Legacy Name", Assert.IsType<S5001ForeignStockLoadRequest>(legacy).StockName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Two_column_alias_routes_futures_to_s5032_and_normalizes_side_order()
|
||||
{
|
||||
|
||||
@@ -260,14 +260,47 @@ public sealed class LegacyRefreshSchedulerTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ZeroMaximum_DisablesAutomaticRefreshBeforeTheFirstDelay()
|
||||
public async Task ZeroMaximum_DisablesAutomaticRefreshBeforeCallbacksOrTheFirstDelay()
|
||||
{
|
||||
var scheduler = CreateScheduler(new ControlledDelay(), maximumRefreshCount: 0);
|
||||
var delays = new ControlledDelay();
|
||||
var scheduler = CreateScheduler(delays, maximumRefreshCount: 0);
|
||||
var callbackCalls = 0;
|
||||
var refreshCalls = 0;
|
||||
|
||||
Assert.Equal(0, scheduler.CompletedRefreshCount);
|
||||
Assert.Equal(0, scheduler.MaximumRefreshCount);
|
||||
Assert.True(scheduler.HasReachedMaximum);
|
||||
Assert.True(scheduler.IsMaximumCallbackDrained);
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||
scheduler.WaitForNextRefreshAsync(
|
||||
_ =>
|
||||
{
|
||||
callbackCalls++;
|
||||
return Task.FromResult(true);
|
||||
},
|
||||
null,
|
||||
CancellationToken.None));
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||
scheduler.ExecuteRefreshAndTrackSuccessAsync(
|
||||
_ =>
|
||||
{
|
||||
refreshCalls++;
|
||||
return Task.FromResult(true);
|
||||
},
|
||||
static result => result,
|
||||
CancellationToken.None));
|
||||
Assert.True(await scheduler.DrainMaximumCallbackAsync(
|
||||
_ =>
|
||||
{
|
||||
callbackCalls++;
|
||||
return Task.FromResult(true);
|
||||
},
|
||||
CancellationToken.None));
|
||||
|
||||
Assert.Equal(0, callbackCalls);
|
||||
Assert.Equal(0, refreshCalls);
|
||||
Assert.Equal(0, delays.CallCount);
|
||||
Assert.Throws<InvalidOperationException>(scheduler.MarkRefreshSucceeded);
|
||||
}
|
||||
|
||||
|
||||
@@ -192,8 +192,8 @@ public sealed class LegacyThemeCatalogPersistenceServiceTests
|
||||
var update = transaction.Commands[0];
|
||||
Assert.Equal("기존 테마", Parameter(update, "expected_title").Value);
|
||||
Assert.Equal("새 테마", Parameter(update, "new_title").Value);
|
||||
Assert.Equal("새 테마", Parameter(update, "duplicate_title").Value);
|
||||
Assert.Contains("NOT EXISTS", update.Sql, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain(update.Parameters, parameter => parameter.Name == "duplicate_title");
|
||||
Assert.DoesNotContain("NOT EXISTS", update.Sql, StringComparison.Ordinal);
|
||||
Assert.Equal(
|
||||
OperatorCatalogAffectedRowsRule.ExactlyOne,
|
||||
update.AffectedRowsRule);
|
||||
@@ -284,7 +284,7 @@ public sealed class LegacyThemeCatalogPersistenceServiceTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Mutations_RejectDuplicateOrAmbiguousItemsBeforeExecutor()
|
||||
public async Task Mutations_AllowRepeatedItemsButRejectInvalidOrderOrMarketBeforeExecutor()
|
||||
{
|
||||
var mutation = new CatalogRecordingMutationExecutor();
|
||||
var service = new LegacyThemeCatalogPersistenceService(UnusedQuery(), mutation);
|
||||
@@ -294,24 +294,17 @@ public sealed class LegacyThemeCatalogPersistenceServiceTests
|
||||
"테마",
|
||||
[]);
|
||||
|
||||
await Assert.ThrowsAsync<ArgumentException>(() => service.CreateAsync(
|
||||
await service.CreateAsync(
|
||||
baseDefinition with
|
||||
{
|
||||
Items =
|
||||
[
|
||||
new ThemeCatalogItem(0, "P005930", "삼성전자"),
|
||||
new ThemeCatalogItem(1, "P005930", "다른이름")
|
||||
new ThemeCatalogItem(1, "P005930", "삼성전자")
|
||||
]
|
||||
}));
|
||||
await Assert.ThrowsAsync<ArgumentException>(() => service.CreateAsync(
|
||||
baseDefinition with
|
||||
{
|
||||
Items =
|
||||
[
|
||||
new ThemeCatalogItem(0, "P005930", "삼성전자"),
|
||||
new ThemeCatalogItem(1, "D035720", "삼성전자")
|
||||
]
|
||||
}));
|
||||
});
|
||||
Assert.Equal(3, Assert.Single(mutation.Calls).Transaction.Commands.Count);
|
||||
mutation.Calls.Clear();
|
||||
await Assert.ThrowsAsync<ArgumentException>(() => service.CreateAsync(
|
||||
baseDefinition with
|
||||
{
|
||||
|
||||
@@ -83,6 +83,7 @@ public sealed class LegacyThemeSelectionServiceTests
|
||||
{
|
||||
call.Spec.ValidateFor(call.Source);
|
||||
Assert.Contains("ESCAPE '!'", call.Spec.Sql, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("ORDER BY", call.Spec.Sql, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Equal(DbType.String, call.Spec.Parameters[0].DbType);
|
||||
Assert.Equal(DbType.Int32, call.Spec.Parameters[1].DbType);
|
||||
});
|
||||
@@ -122,7 +123,7 @@ public sealed class LegacyThemeSelectionServiceTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SearchAsync_AppliesCombinedBoundAndDeterministicMarketOrder()
|
||||
public async Task SearchAsync_AppliesCombinedBoundAndPreservesKrxProviderOrderBeforeNxt()
|
||||
{
|
||||
var executor = new RecordingExecutor(call => call.Source switch
|
||||
{
|
||||
@@ -138,7 +139,7 @@ public sealed class LegacyThemeSelectionServiceTests
|
||||
|
||||
Assert.True(result.IsTruncated);
|
||||
Assert.Equal(
|
||||
["Alpha", "Zulu"],
|
||||
["Zulu", "Alpha"],
|
||||
result.Items.Select(static item => item.Identity.ThemeTitle));
|
||||
Assert.All(executor.Calls, call => Assert.Equal(3, call.Spec.Parameters[1].Value));
|
||||
}
|
||||
@@ -229,9 +230,8 @@ public sealed class LegacyThemeSelectionServiceTests
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("AI", "00000001", "AI", "00000002")]
|
||||
[InlineData("AI", "00000001", "Robotics", "00000001")]
|
||||
public async Task SearchAsync_RejectsDuplicateTitleOrCodeWithinMarket(
|
||||
public async Task SearchAsync_RejectsDuplicateCodeWithinMarket(
|
||||
string title1,
|
||||
string code1,
|
||||
string title2,
|
||||
@@ -245,7 +245,22 @@ public sealed class LegacyThemeSelectionServiceTests
|
||||
var exception = await Assert.ThrowsAsync<ThemeSelectionDataException>(
|
||||
() => service.SearchAsync("", ThemeSession.PreMarket));
|
||||
|
||||
Assert.Contains("duplicate title or code", exception.Message, StringComparison.Ordinal);
|
||||
Assert.Contains("duplicate theme code", exception.Message, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SearchAsync_AllowsDuplicateTitlesWithDistinctCodes()
|
||||
{
|
||||
var executor = new RecordingExecutor(call => call.Source == DataSourceKind.Oracle
|
||||
? SearchTable(
|
||||
("AI", "00000001", "KRX"),
|
||||
("AI", "00000002", "KRX"))
|
||||
: SearchTable());
|
||||
|
||||
var result = await new LegacyThemeSelectionService(executor)
|
||||
.SearchAsync("", ThemeSession.PreMarket);
|
||||
|
||||
Assert.Equal(2, result.Items.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -431,10 +446,9 @@ public sealed class LegacyThemeSelectionServiceTests
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("P005930", "0", "P005930", "1")]
|
||||
[InlineData("P005930", "0", "D035720", "0")]
|
||||
[InlineData("P005930", "2", "D035720", "1")]
|
||||
public async Task PreviewAsync_RejectsDuplicateOrNonIncreasingItemIdentity(
|
||||
public async Task PreviewAsync_RejectsDuplicateOrNonIncreasingItemIndex(
|
||||
string code1,
|
||||
string index1,
|
||||
string code2,
|
||||
@@ -449,6 +463,19 @@ public sealed class LegacyThemeSelectionServiceTests
|
||||
() => service.PreviewAsync(KrxIdentity()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PreviewAsync_AllowsRepeatedItemAtDistinctIndexes()
|
||||
{
|
||||
var executor = new RecordingExecutor(_ => PreviewTable(
|
||||
("AI", "00000001", "KRX", "Samsung", "P005930", "0"),
|
||||
("AI", "00000001", "KRX", "Samsung", "P005930", "1")));
|
||||
|
||||
var result = await new LegacyThemeSelectionService(executor)
|
||||
.PreviewAsync(KrxIdentity());
|
||||
|
||||
Assert.Equal(2, result.Items.Count);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("01")]
|
||||
[InlineData("-1")]
|
||||
|
||||
@@ -130,6 +130,28 @@ public sealed class ParameterizedMarketSceneDataLoadersTests
|
||||
Assert.Equal(stockName, Assert.Single(stock.Spec.Parameters).Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Exact_foreign_stock_identity_allows_any_two_letter_nation_without_name_collision()
|
||||
{
|
||||
const string name = "Shared Name";
|
||||
const string symbol = "LNS@LON";
|
||||
const string nation = "GB";
|
||||
var executor = new RecordingExecutor(QuoteTable(name, 100m, "+", 1m, 1m));
|
||||
|
||||
var data = await new S5001SceneDataLoader(executor).LoadAsync(
|
||||
new S5001ExactForeignStockLoadRequest(name, symbol, nation));
|
||||
|
||||
Assert.IsType<S5001ForeignStockSceneData>(data);
|
||||
var call = Assert.Single(executor.Calls);
|
||||
Assert.Equal("SCENE_FOREIGN_STOCK", call.TableName);
|
||||
Assert.Contains("b.f_symb = :symbol", call.Spec.Sql, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("b.f_natc = :nationCode", call.Spec.Sql, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.DoesNotContain("b.f_natc IN ('US', 'TW')", call.Spec.Sql, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Equal(
|
||||
[name, symbol, nation],
|
||||
call.Spec.Parameters.Select(parameter => parameter.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Closed_foreign_index_symbols_keep_global_markets_but_reject_stock_master_rows()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user