feat: complete legacy parity and modernize operator UI
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
using System.Data;
|
||||
using MBN_STOCK_WEBVIEW.Core.Playout.Scenes;
|
||||
using MMoneyCoderSharp.Data;
|
||||
|
||||
@@ -5,6 +6,106 @@ namespace MBN_STOCK_WEBVIEW.LegacyApplication.Tests;
|
||||
|
||||
public sealed class LegacyManualFinancialOperatorControllerTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task GraphE_open_warns_when_invalid_legacy_rows_were_isolated()
|
||||
{
|
||||
var service = new FakeManualService(Sales("Alpha", 1))
|
||||
{
|
||||
RejectedItemCount = 2
|
||||
};
|
||||
|
||||
var opened = await Controller(service).OpenManualFinancialAsync(
|
||||
ManualFinancialScreenKind.Sales);
|
||||
|
||||
Assert.Single(opened.ManualFinancial!.Rows);
|
||||
Assert.Equal(2, opened.ManualFinancial.ListRejectedItemCount);
|
||||
Assert.Equal(LegacyOperatorStatusKind.Warning, opened.StatusKind);
|
||||
Assert.Contains("2행", opened.StatusMessage, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Strict_ready_GraphE_load_clears_previous_raw_row_warning()
|
||||
{
|
||||
var service = new LegacyManualFinancialScreenService(
|
||||
new RawGraphEQueryExecutor());
|
||||
var controller = Controller(service);
|
||||
var opened = await controller.OpenManualFinancialAsync(
|
||||
ManualFinancialScreenKind.Sales);
|
||||
var rawRow = opened.ManualFinancial!.Rows.Single(row => !row.IsPlayoutReady);
|
||||
var readyRow = opened.ManualFinancial.Rows.Single(row => row.IsPlayoutReady);
|
||||
|
||||
var rawLoaded = await controller.LoadManualFinancialAsync(rawRow.ResultId);
|
||||
|
||||
Assert.False(rawLoaded.ManualFinancial!.IsPlayoutReady);
|
||||
Assert.Equal(LegacyOperatorStatusKind.Warning, rawLoaded.StatusKind);
|
||||
Assert.Contains("원문", rawLoaded.StatusMessage, StringComparison.Ordinal);
|
||||
|
||||
var readyLoaded = await controller.LoadManualFinancialAsync(readyRow.ResultId);
|
||||
|
||||
Assert.True(readyLoaded.ManualFinancial!.IsPlayoutReady);
|
||||
Assert.Equal(LegacyOperatorStatusKind.Information, readyLoaded.StatusKind);
|
||||
Assert.Contains("Alpha", readyLoaded.StatusMessage, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("원문", readyLoaded.StatusMessage, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Unreadable_row_failure_clears_previous_detail_and_playout_authority()
|
||||
{
|
||||
var service = new FakeManualService(Sales("Alpha", 1))
|
||||
{
|
||||
RejectedItemCount = 1,
|
||||
UnreadableStockNames = ["Broken"],
|
||||
GetOverride = identity => identity.StockName == "Broken"
|
||||
? throw new ManualFinancialDataException("Unreadable legacy row.")
|
||||
: Sales("Alpha", 1)
|
||||
};
|
||||
var controller = Controller(service);
|
||||
var opened = await controller.OpenManualFinancialAsync(
|
||||
ManualFinancialScreenKind.Sales);
|
||||
var valid = opened.ManualFinancial!.Rows.Single(row => row.IsDataReadable);
|
||||
var unreadable = opened.ManualFinancial.Rows.Single(row => !row.IsDataReadable);
|
||||
var loaded = await controller.LoadManualFinancialAsync(valid.ResultId);
|
||||
Assert.True(loaded.ManualFinancial!.CanMaterializePlaylist);
|
||||
|
||||
var rejected = await controller.LoadManualFinancialAsync(unreadable.ResultId);
|
||||
|
||||
var failed = rejected.ManualFinancial!;
|
||||
Assert.Equal(unreadable.ResultId, failed.SelectedRowId);
|
||||
Assert.Null(failed.SelectedRecord);
|
||||
Assert.False(failed.CanSave);
|
||||
Assert.False(failed.CanDelete);
|
||||
Assert.False(failed.CanMaterializePlaylist);
|
||||
Assert.NotNull(rejected.Dialog);
|
||||
|
||||
var activated = await controller.ActivateManualFinancialResultAsync(
|
||||
unreadable.ResultId);
|
||||
Assert.Empty(activated.Playlist);
|
||||
Assert.False(activated.CommandResult?.Succeeded);
|
||||
Assert.Equal(unreadable.ResultId, activated.CommandResult?.TargetId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Stale_result_handle_clears_previous_detail_before_it_is_rejected()
|
||||
{
|
||||
var service = new FakeManualService(Sales("Alpha", 1));
|
||||
var controller = Controller(service);
|
||||
var opened = await controller.OpenManualFinancialAsync(
|
||||
ManualFinancialScreenKind.Sales);
|
||||
var valid = Assert.Single(opened.ManualFinancial!.Rows);
|
||||
var loaded = await controller.LoadManualFinancialAsync(valid.ResultId);
|
||||
Assert.True(loaded.ManualFinancial!.CanMaterializePlaylist);
|
||||
|
||||
var rejected = await controller.LoadManualFinancialAsync("stale-result-id");
|
||||
|
||||
Assert.Null(rejected.ManualFinancial!.SelectedRowId);
|
||||
Assert.Null(rejected.ManualFinancial.SelectedRecord);
|
||||
Assert.False(rejected.ManualFinancial.CanSave);
|
||||
Assert.False(rejected.ManualFinancial.CanDelete);
|
||||
Assert.False(rejected.ManualFinancial.CanMaterializePlaylist);
|
||||
Assert.NotNull(rejected.Dialog);
|
||||
Assert.Equal(1, service.GetCalls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GraphE_result_double_click_fresh_loads_appends_once_and_closes_on_success()
|
||||
{
|
||||
@@ -116,11 +217,96 @@ public sealed class LegacyManualFinancialOperatorControllerTests
|
||||
LegacyManualFinancialWorkflowStatus.WriteCommitted,
|
||||
saved.ManualFinancial!.Status);
|
||||
Assert.True(saved.ManualFinancial.CanMaterializePlaylist);
|
||||
Assert.Equal("저장되었습니다", saved.Dialog?.Message);
|
||||
Assert.Equal("MmoneyCoder", saved.Dialog?.Caption);
|
||||
Assert.False(saved.Dialog?.IsConfirmation);
|
||||
Assert.DoesNotContain("password", saved.StatusMessage, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
var dismissed = controller.DismissDialog();
|
||||
Assert.Null(dismissed.Dialog);
|
||||
Assert.NotNull(dismissed.ManualFinancial);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Delete_and_delete_all_are_only_reached_by_explicit_controller_methods()
|
||||
public async Task Empty_GraphE_stock_confirm_is_a_quiet_no_op()
|
||||
{
|
||||
var service = new FakeManualService();
|
||||
var controller = Controller(service);
|
||||
var opened = await controller.OpenManualFinancialAsync(
|
||||
ManualFinancialScreenKind.OperatingProfit);
|
||||
var creating = controller.BeginManualFinancialCreate();
|
||||
|
||||
var unchanged = await controller.SearchManualFinancialStocksAsync(" ");
|
||||
|
||||
Assert.Equal(creating.Revision, unchanged.Revision);
|
||||
Assert.Equal(creating.ManualFinancial!.Revision, unchanged.ManualFinancial!.Revision);
|
||||
Assert.Empty(unchanged.ManualFinancial.StockCandidates);
|
||||
Assert.Null(unchanged.Dialog);
|
||||
Assert.Equal(0, service.MutationCalls);
|
||||
Assert.NotEqual(opened.Revision, unchanged.Revision);
|
||||
|
||||
var noSelection = controller.SelectManualFinancialStock(string.Empty);
|
||||
Assert.Equal(unchanged.Revision, noSelection.Revision);
|
||||
Assert.Null(noSelection.Dialog);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Known_GraphE_save_rejection_uses_specific_sanitized_dialog_and_keeps_editor()
|
||||
{
|
||||
const string safeMessage =
|
||||
"The create precondition failed and the Oracle transaction was rolled back.";
|
||||
var service = new FakeManualService
|
||||
{
|
||||
MutationException = new ManualFinancialMutationRejectedException(
|
||||
ManualFinancialMutationRejectionReason.DuplicateIdentity,
|
||||
safeMessage)
|
||||
};
|
||||
var controller = Controller(service);
|
||||
await controller.OpenManualFinancialAsync(ManualFinancialScreenKind.OperatingProfit);
|
||||
controller.BeginManualFinancialCreate();
|
||||
var searched = await controller.SearchManualFinancialStocksAsync("Alpha");
|
||||
controller.SelectManualFinancialStock(
|
||||
Assert.Single(searched.ManualFinancial!.StockCandidates).ResultId);
|
||||
|
||||
var rejected = await controller.SaveManualFinancialAsync(Profit("Alpha", 10));
|
||||
|
||||
Assert.Equal(1, service.MutationCalls);
|
||||
Assert.Equal(
|
||||
LegacyManualFinancialWorkflowStatus.WriteRejected,
|
||||
rejected.ManualFinancial!.Status);
|
||||
Assert.Equal(safeMessage, rejected.StatusMessage);
|
||||
Assert.Equal(safeMessage, rejected.Dialog?.Message);
|
||||
Assert.Equal("MmoneyCoder", rejected.Dialog?.Caption);
|
||||
Assert.DoesNotContain("DB연결", rejected.Dialog!.Message, StringComparison.Ordinal);
|
||||
|
||||
var dismissed = controller.DismissDialog();
|
||||
Assert.Null(dismissed.Dialog);
|
||||
Assert.NotNull(dismissed.ManualFinancial);
|
||||
Assert.True(dismissed.ManualFinancial.CanSave);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Malformed_GraphE_editor_data_is_not_misreported_as_a_database_outage()
|
||||
{
|
||||
var service = new FakeManualService();
|
||||
var controller = Controller(service);
|
||||
await controller.OpenManualFinancialAsync(ManualFinancialScreenKind.OperatingProfit);
|
||||
controller.BeginManualFinancialCreate();
|
||||
var searched = await controller.SearchManualFinancialStocksAsync("Alpha");
|
||||
controller.SelectManualFinancialStock(
|
||||
Assert.Single(searched.ManualFinancial!.StockCandidates).ResultId);
|
||||
|
||||
var rejected = await controller.SaveManualFinancialAsync(
|
||||
Assert.IsType<ManualSalesRecord>(Sales("Alpha", 1).Record));
|
||||
|
||||
Assert.NotNull(rejected.ManualFinancial);
|
||||
Assert.NotNull(rejected.Dialog);
|
||||
Assert.DoesNotContain("DB연결", rejected.Dialog!.Message, StringComparison.Ordinal);
|
||||
Assert.Equal(0, service.MutationCalls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Delete_and_delete_all_require_MmoneyCoder_confirmation_before_writing()
|
||||
{
|
||||
var service = new FakeManualService(Sales("Alpha", 1));
|
||||
var controller = Controller(service);
|
||||
@@ -129,16 +315,47 @@ public sealed class LegacyManualFinancialOperatorControllerTests
|
||||
Assert.Single(opened.ManualFinancial!.Rows).ResultId);
|
||||
|
||||
Assert.Equal(0, service.MutationCalls);
|
||||
var deleted = await controller.DeleteManualFinancialAsync(all: false);
|
||||
var deleteRequested = await controller.DeleteManualFinancialAsync(all: false);
|
||||
Assert.Equal("선택한 종목을 삭제하겠습니까?", deleteRequested.Dialog?.Message);
|
||||
Assert.Equal("MmoneyCoder", deleteRequested.Dialog?.Caption);
|
||||
Assert.True(deleteRequested.Dialog?.IsConfirmation);
|
||||
Assert.Equal(0, service.MutationCalls);
|
||||
|
||||
var canceled = controller.CancelDialog();
|
||||
Assert.Null(canceled.Dialog);
|
||||
Assert.Equal(0, service.MutationCalls);
|
||||
Assert.Single(canceled.ManualFinancial!.Rows);
|
||||
|
||||
await controller.DeleteManualFinancialAsync(all: false);
|
||||
var deleted = await controller.ConfirmDialogAsync();
|
||||
Assert.Equal(1, service.MutationCalls);
|
||||
Assert.Empty(deleted.ManualFinancial!.Rows);
|
||||
|
||||
var all = await controller.DeleteManualFinancialAsync(all: true);
|
||||
var allRequested = await controller.DeleteManualFinancialAsync(all: true);
|
||||
Assert.Equal("모든 종목을 삭제하겠습니까?", allRequested.Dialog?.Message);
|
||||
Assert.True(allRequested.Dialog?.IsConfirmation);
|
||||
Assert.Equal(1, service.MutationCalls);
|
||||
|
||||
var all = await controller.ConfirmDialogAsync();
|
||||
Assert.Equal(2, service.MutationCalls);
|
||||
Assert.Empty(all.ManualFinancial!.Rows);
|
||||
}
|
||||
|
||||
private static LegacyOperatorController Controller(FakeManualService service)
|
||||
[Fact]
|
||||
public async Task Delete_without_a_GraphE_row_selection_remains_a_silent_no_op()
|
||||
{
|
||||
var service = new FakeManualService(Sales("Alpha", 1));
|
||||
var controller = Controller(service);
|
||||
await controller.OpenManualFinancialAsync(ManualFinancialScreenKind.Sales);
|
||||
|
||||
var result = await controller.DeleteManualFinancialAsync(all: false);
|
||||
|
||||
Assert.Null(result.Dialog);
|
||||
Assert.Equal(0, service.MutationCalls);
|
||||
Assert.Single(result.ManualFinancial!.Rows);
|
||||
}
|
||||
|
||||
private static LegacyOperatorController Controller(IManualFinancialScreenService service)
|
||||
{
|
||||
var workflow = new LegacyManualFinancialWorkflow(
|
||||
service,
|
||||
@@ -214,6 +431,13 @@ public sealed class LegacyManualFinancialOperatorControllerTests
|
||||
|
||||
internal int GetCalls { get; private set; }
|
||||
|
||||
internal ManualFinancialMutationException? MutationException { get; init; }
|
||||
|
||||
internal int RejectedItemCount { get; init; }
|
||||
|
||||
internal IReadOnlyList<string> UnreadableStockNames { get; init; } =
|
||||
Array.Empty<string>();
|
||||
|
||||
internal Func<ManualFinancialIdentity, ManualFinancialSnapshot>? GetOverride
|
||||
{
|
||||
get;
|
||||
@@ -239,7 +463,11 @@ public sealed class LegacyManualFinancialOperatorControllerTests
|
||||
query,
|
||||
DateTimeOffset.UtcNow,
|
||||
rows,
|
||||
false));
|
||||
false)
|
||||
{
|
||||
RejectedItemCount = RejectedItemCount,
|
||||
UnreadableStockNames = UnreadableStockNames
|
||||
});
|
||||
}
|
||||
|
||||
public Task<ManualFinancialSnapshot> GetAsync(
|
||||
@@ -257,6 +485,11 @@ public sealed class LegacyManualFinancialOperatorControllerTests
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
MutationCalls++;
|
||||
if (MutationException is not null)
|
||||
{
|
||||
throw MutationException;
|
||||
}
|
||||
|
||||
_rows[(record.Identity.Screen, record.Identity.StockName)] =
|
||||
new ManualFinancialSnapshot(record, Version(++_version));
|
||||
return Task.FromResult(Receipt(
|
||||
@@ -272,6 +505,11 @@ public sealed class LegacyManualFinancialOperatorControllerTests
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
MutationCalls++;
|
||||
if (MutationException is not null)
|
||||
{
|
||||
throw MutationException;
|
||||
}
|
||||
|
||||
_rows[(replacement.Identity.Screen, replacement.Identity.StockName)] =
|
||||
new ManualFinancialSnapshot(replacement, Version(++_version));
|
||||
return Task.FromResult(Receipt(
|
||||
@@ -327,4 +565,74 @@ public sealed class LegacyManualFinancialOperatorControllerTests
|
||||
DateTimeOffset.UtcNow,
|
||||
affectedRows);
|
||||
}
|
||||
|
||||
private sealed class RawGraphEQueryExecutor : IDataQueryExecutor
|
||||
{
|
||||
private readonly DataTable _rows = CreateRows();
|
||||
|
||||
public DataTable Execute(DataSourceKind source, string tableName, string query) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public Task<DataTable> ExecuteAsync(
|
||||
DataSourceKind source,
|
||||
string tableName,
|
||||
string query,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public Task<DataTable> ExecuteAsync(
|
||||
DataSourceKind source,
|
||||
string tableName,
|
||||
DataQuerySpec query,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (!string.Equals(
|
||||
tableName,
|
||||
"MANUAL_FINANCIAL_SELL_RAW_GET",
|
||||
StringComparison.Ordinal))
|
||||
{
|
||||
return Task.FromResult(_rows.Copy());
|
||||
}
|
||||
|
||||
var stockName = Assert.IsType<string>(query.Parameters.Single(
|
||||
parameter => string.Equals(
|
||||
parameter.Name,
|
||||
"stock_name",
|
||||
StringComparison.OrdinalIgnoreCase)).Value);
|
||||
var result = _rows.Clone();
|
||||
var row = Assert.Single(_rows.AsEnumerable().Where(candidate =>
|
||||
string.Equals(
|
||||
candidate.Field<string>("STOCK_NAME"),
|
||||
stockName,
|
||||
StringComparison.Ordinal)));
|
||||
result.ImportRow(row);
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
private static DataTable CreateRows()
|
||||
{
|
||||
var table = new DataTable();
|
||||
foreach (var column in ManualFinancialCutContracts
|
||||
.Get(ManualFinancialScreenKind.Sales)
|
||||
.StorageColumns)
|
||||
{
|
||||
table.Columns.Add(column, typeof(string));
|
||||
}
|
||||
|
||||
table.Columns.Add("ROW_VERSION", typeof(string));
|
||||
table.Columns.Add("ROW_HANDLE", typeof(string));
|
||||
table.Rows.Add(
|
||||
"Alpha",
|
||||
"1Q_10", "2Q_20", "3Q_30", "4Q_40", "5Q_50", "6Q_60",
|
||||
Version(1),
|
||||
"AAAAAAAAAAAAAAAA01");
|
||||
table.Rows.Add(
|
||||
"Broken",
|
||||
"1Q_not-a-number", "2Q_20", "3Q_30", "4Q_40", "5Q_50", "6Q_60",
|
||||
Version(2),
|
||||
"AAAAAAAAAAAAAAAA02");
|
||||
return table;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,57 @@
|
||||
using System.Data;
|
||||
using MMoneyCoderSharp.Data;
|
||||
|
||||
namespace MBN_STOCK_WEBVIEW.LegacyApplication.Tests;
|
||||
|
||||
public sealed class LegacyManualFinancialWorkflowTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task GraphE_stock_picker_uses_the_complete_finite_core_boundary()
|
||||
{
|
||||
var service = new FakeManualFinancialService();
|
||||
var stockSearch = new FakeStockSearchService();
|
||||
var workflow = new LegacyManualFinancialWorkflow(
|
||||
service,
|
||||
stockSearch,
|
||||
new LegacyManualFinancialWriteSafetyState());
|
||||
|
||||
await workflow.SearchStocksAsync(
|
||||
workflow.CreateInitial(ManualFinancialScreenKind.Sales),
|
||||
"Alpha");
|
||||
|
||||
Assert.Equal(
|
||||
LegacyStockSearchService.MaximumResults,
|
||||
LegacyManualFinancialWorkflow.MaximumStockResults);
|
||||
Assert.Equal(
|
||||
LegacyStockSearchService.MaximumResults,
|
||||
stockSearch.LastMaximumResults);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Empty_GraphE_stock_search_is_the_original_quiet_no_op()
|
||||
{
|
||||
var stockSearch = new FakeStockSearchService(
|
||||
new StockSearchItem(
|
||||
StockMarket.Kospi,
|
||||
DataSourceKind.Oracle,
|
||||
"Alpha",
|
||||
"000001"));
|
||||
var workflow = new LegacyManualFinancialWorkflow(
|
||||
new FakeManualFinancialService(),
|
||||
stockSearch,
|
||||
new LegacyManualFinancialWriteSafetyState());
|
||||
var initial = workflow.CreateInitial(ManualFinancialScreenKind.Sales);
|
||||
|
||||
var unchanged = await workflow.SearchStocksAsync(initial, " ");
|
||||
|
||||
Assert.Same(initial, unchanged);
|
||||
Assert.Equal(0, stockSearch.LastMaximumResults);
|
||||
Assert.Empty(unchanged.StockCandidates);
|
||||
Assert.Null(unchanged.LastMessage);
|
||||
|
||||
Assert.Same(unchanged, workflow.SelectStock(unchanged, string.Empty));
|
||||
}
|
||||
|
||||
public static TheoryData<
|
||||
ManualFinancialScreenKind,
|
||||
string,
|
||||
@@ -93,6 +141,9 @@ public sealed class LegacyManualFinancialWorkflowTests
|
||||
Assert.Equal(LegacyManualFinancialWorkflow.MaximumListResults, service.LastMaximumResults);
|
||||
var row = Assert.Single(state.Rows);
|
||||
Assert.StartsWith("mf-row-", row.ResultId, StringComparison.Ordinal);
|
||||
Assert.Equal(
|
||||
["Alpha", "1Q_1", "2Q_2", "3Q_3", "4Q_4", "5Q_5", "6Q_6"],
|
||||
row.PreviewValues);
|
||||
Assert.Null(state.SelectedRecord);
|
||||
Assert.False(state.IsDetailFresh);
|
||||
|
||||
@@ -106,6 +157,89 @@ public sealed class LegacyManualFinancialWorkflowTests
|
||||
Assert.Equal(Version(2), Assert.Single(state.Rows).RowVersion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Search_surfaces_isolated_legacy_row_count_without_hiding_valid_rows()
|
||||
{
|
||||
var valid = Snapshot(Record(ManualFinancialScreenKind.Sales, "Alpha"), 1);
|
||||
var service = new FakeManualFinancialService(valid)
|
||||
{
|
||||
SearchOverride = (screen, query) => new ManualFinancialSearchResult(
|
||||
screen,
|
||||
query,
|
||||
DateTimeOffset.UtcNow,
|
||||
[valid],
|
||||
false)
|
||||
{
|
||||
RejectedItemCount = 3,
|
||||
UnreadableStockNames = ["Broken", "BadVersion", "Legacy"]
|
||||
}
|
||||
};
|
||||
var workflow = Workflow(service);
|
||||
|
||||
var state = await workflow.SearchAsync(
|
||||
workflow.CreateInitial(ManualFinancialScreenKind.Sales));
|
||||
|
||||
Assert.Equal(4, state.Rows.Count);
|
||||
Assert.Single(state.Rows, row => row.IsDataReadable);
|
||||
Assert.Equal(3, state.Rows.Count(row => !row.IsDataReadable));
|
||||
Assert.Equal(3, state.ListRejectedItemCount);
|
||||
Assert.Contains("3행", state.LastMessage, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Raw_GraphE_row_can_edit_delete_and_schedule_while_playout_stays_unready()
|
||||
{
|
||||
var table = RawSalesTable("Broken", "1Q_not-a-number");
|
||||
var service = new LegacyManualFinancialScreenService(
|
||||
new StaticQueryExecutor(table),
|
||||
new NoCallMutationExecutor());
|
||||
var workflow = new LegacyManualFinancialWorkflow(
|
||||
service,
|
||||
new FakeStockSearchService(
|
||||
new StockSearchItem(
|
||||
StockMarket.Kospi,
|
||||
DataSourceKind.Oracle,
|
||||
"Broken",
|
||||
"000001")),
|
||||
new LegacyManualFinancialWriteSafetyState());
|
||||
var state = await workflow.SearchAsync(
|
||||
workflow.CreateInitial(ManualFinancialScreenKind.Sales));
|
||||
var row = Assert.Single(state.Rows);
|
||||
Assert.True(row.IsDataReadable);
|
||||
Assert.False(row.IsPlayoutReady);
|
||||
Assert.Equal(
|
||||
["Broken", "1Q_not-a-number", "2Q_20", "3Q_30", "4Q_40", "5Q_50", "6Q_60"],
|
||||
row.PreviewValues);
|
||||
Assert.Throws<NotSupportedException>(() =>
|
||||
((IList<string>)row.PreviewValues).Add("ROWID"));
|
||||
|
||||
state = await workflow.LoadAsync(state, row.ResultId);
|
||||
Assert.True(state.IsRawDetailFresh);
|
||||
Assert.False(state.IsDetailFresh);
|
||||
Assert.False(state.IsPlayoutReady);
|
||||
Assert.NotNull(state.SelectedRawRecord);
|
||||
Assert.Null(state.SelectedRecord);
|
||||
|
||||
state = await workflow.SearchStocksAsync(state, "Broken");
|
||||
state = workflow.SelectStock(state, Assert.Single(state.StockCandidates).ResultId);
|
||||
Assert.True(state.CanEdit);
|
||||
Assert.True(state.CanSave);
|
||||
Assert.True(state.CanDelete);
|
||||
Assert.True(state.CanMaterializePlaylist);
|
||||
|
||||
var replacement = new ManualFinancialRawRecord(
|
||||
state.SelectedRawRecord!.Identity,
|
||||
state.SelectedRawRecord.StorageValues.ToArray());
|
||||
Assert.IsType<LegacyManualFinancialRawUpdateRequest>(
|
||||
workflow.CreateRawUpdateRequest(state, replacement));
|
||||
Assert.IsType<LegacyManualFinancialRawDeleteRequest>(
|
||||
workflow.CreateDeleteRequest(state));
|
||||
var draft = workflow.MaterializePlaylistDraft(state, "sales");
|
||||
Assert.Null(draft.OperatorSnapshot);
|
||||
Assert.NotNull(draft.OperatorRawSnapshot);
|
||||
Assert.Equal("Broken", draft.CutSelection.Selection.Subject);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GraphE_name_header_and_directional_find_are_CSharp_authoritative()
|
||||
{
|
||||
@@ -193,7 +327,7 @@ public sealed class LegacyManualFinancialWorkflowTests
|
||||
Assert.Equal(code, draft.CutSelection.CutCode);
|
||||
Assert.Equal(1, draft.CutSelection.CurrentPage);
|
||||
Assert.Equal(1, draft.CutSelection.TotalPages);
|
||||
Assert.Equal(state.SelectedRowVersion, draft.OperatorSnapshot.RowVersion);
|
||||
Assert.Equal(state.SelectedRowVersion, draft.OperatorSnapshot!.RowVersion);
|
||||
Assert.Equal("KOSPI", draft.Market);
|
||||
Assert.Equal("000001", draft.StockCode);
|
||||
Assert.False(draft.IsNxt);
|
||||
@@ -220,7 +354,7 @@ public sealed class LegacyManualFinancialWorkflowTests
|
||||
Assert.Same(replacement.Identity, replacement.Identity);
|
||||
Assert.NotSame(replacement, request.Replacement);
|
||||
var draft = workflow.MaterializePlaylistDraft(state, "growth-metrics");
|
||||
Assert.Equal(Version(11), draft.OperatorSnapshot.RowVersion);
|
||||
Assert.Equal(Version(11), draft.OperatorSnapshot!.RowVersion);
|
||||
Assert.Equal(1, service.MutationCalls);
|
||||
Assert.Equal(2, service.GetCalls);
|
||||
}
|
||||
@@ -525,7 +659,7 @@ public sealed class LegacyManualFinancialWorkflowTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Invalid_or_ambiguous_list_envelopes_are_rejected()
|
||||
public async Task Duplicate_list_names_remain_visible_but_invalid_unreadable_identity_is_rejected()
|
||||
{
|
||||
var duplicate = Snapshot(Record(ManualFinancialScreenKind.Sales, "Alpha"), 1);
|
||||
var service = new FakeManualFinancialService
|
||||
@@ -539,6 +673,21 @@ public sealed class LegacyManualFinancialWorkflowTests
|
||||
};
|
||||
var workflow = Workflow(service);
|
||||
|
||||
var duplicates = await workflow.SearchAsync(
|
||||
workflow.CreateInitial(ManualFinancialScreenKind.Sales));
|
||||
Assert.Equal(2, duplicates.Rows.Count);
|
||||
|
||||
service.SearchOverride = (_, query) => new ManualFinancialSearchResult(
|
||||
ManualFinancialScreenKind.Sales,
|
||||
query,
|
||||
DateTimeOffset.UtcNow,
|
||||
[],
|
||||
false)
|
||||
{
|
||||
RejectedItemCount = 1,
|
||||
UnreadableStockNames = [" invalid "]
|
||||
};
|
||||
|
||||
await Assert.ThrowsAsync<LegacyManualFinancialWorkflowDataException>(() =>
|
||||
workflow.SearchAsync(workflow.CreateInitial(ManualFinancialScreenKind.Sales)));
|
||||
}
|
||||
@@ -677,6 +826,65 @@ public sealed class LegacyManualFinancialWorkflowTests
|
||||
};
|
||||
}
|
||||
|
||||
private static DataTable RawSalesTable(string stockName, string firstValue)
|
||||
{
|
||||
var table = new DataTable();
|
||||
foreach (var column in ManualFinancialCutContracts.Get(
|
||||
ManualFinancialScreenKind.Sales).StorageColumns)
|
||||
{
|
||||
table.Columns.Add(column, typeof(string));
|
||||
}
|
||||
table.Columns.Add("ROW_VERSION", typeof(string));
|
||||
table.Columns.Add("ROW_HANDLE", typeof(string));
|
||||
table.Rows.Add(
|
||||
stockName,
|
||||
firstValue,
|
||||
"2Q_20",
|
||||
"3Q_30",
|
||||
"4Q_40",
|
||||
"5Q_50",
|
||||
"6Q_60",
|
||||
Version(1),
|
||||
"AAAAAAAAAAAAAAAAAA");
|
||||
return table;
|
||||
}
|
||||
|
||||
private sealed class StaticQueryExecutor : IDataQueryExecutor
|
||||
{
|
||||
private readonly DataTable _table;
|
||||
|
||||
internal StaticQueryExecutor(DataTable table) => _table = table;
|
||||
|
||||
public DataTable Execute(DataSourceKind source, string tableName, string query) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public Task<DataTable> ExecuteAsync(
|
||||
DataSourceKind source,
|
||||
string tableName,
|
||||
string query,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public Task<DataTable> ExecuteAsync(
|
||||
DataSourceKind source,
|
||||
string tableName,
|
||||
DataQuerySpec query,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return Task.FromResult(_table.Copy());
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class NoCallMutationExecutor : IManualFinancialMutationExecutor
|
||||
{
|
||||
public Task<ManualFinancialDbTransactionResult> ExecuteTransactionAsync(
|
||||
DataSourceKind source,
|
||||
ManualFinancialDbTransaction transaction,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
throw new InvalidOperationException("The test must not issue a real mutation.");
|
||||
}
|
||||
|
||||
private sealed class FakeStockSearchService : IStockSearchService
|
||||
{
|
||||
private readonly IReadOnlyList<StockSearchItem> _items;
|
||||
@@ -686,12 +894,15 @@ public sealed class LegacyManualFinancialWorkflowTests
|
||||
_items = items;
|
||||
}
|
||||
|
||||
internal int LastMaximumResults { get; private set; }
|
||||
|
||||
public Task<StockSearchResult> SearchAsync(
|
||||
string query,
|
||||
int maximumResults = LegacyStockSearchService.DefaultMaximumResults,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
LastMaximumResults = maximumResults;
|
||||
return Task.FromResult(new StockSearchResult(
|
||||
query,
|
||||
DateTimeOffset.UtcNow,
|
||||
|
||||
Reference in New Issue
Block a user