Files
MBN_STOCK_WEBVIEW/tests/MBN_STOCK_WEBVIEW.Infrastructure.Tests/LegacyManualOperatorDataImporterTests.cs

567 lines
23 KiB
C#

using System.Text.Json;
using MBN_STOCK_WEBVIEW.Core.Playout.Scenes;
using MBN_STOCK_WEBVIEW.Infrastructure;
using MMoneyCoderSharp.Data;
namespace MBN_STOCK_WEBVIEW.Infrastructure.Tests;
public sealed class LegacyManualOperatorDataImporterTests
{
private static readonly string[] DestinationFiles =
[
"개인.dat",
"외국인.dat",
"기관.dat",
"VI발동.dat"
];
[Fact]
public async Task Fixed_source_reads_the_four_original_files_without_modifying_them()
{
using var sourceDirectory = new TemporaryDirectory();
WriteSourceFiles(sourceDirectory.Path);
var before = DestinationFiles.ToDictionary(
name => name,
name => File.ReadAllBytes(Path.Combine(sourceDirectory.Path, name)));
var source = new FixedLegacyManualOperatorDataSource(
sourceDirectory.Path,
TimeSpan.Zero);
var snapshot = await source.ReadAsync();
Assert.Equal(5, snapshot.IndividualRows.Count);
Assert.Equal(5, snapshot.ForeignRows.Count);
Assert.Equal(5, snapshot.InstitutionRows.Count);
Assert.Equal(3, snapshot.ViItems.Count);
Assert.Matches("^[a-f0-9]{64}$", snapshot.SourceSha256);
Assert.All(DestinationFiles, name =>
Assert.Equal(before[name], File.ReadAllBytes(Path.Combine(sourceDirectory.Path, name))));
}
[Fact]
public async Task Import_commits_all_four_files_and_the_versioned_marker_last()
{
using var sourceDirectory = new TemporaryDirectory();
using var destinationDirectory = new TemporaryDirectory();
WriteSourceFiles(sourceDirectory.Path);
PreparePrivate(destinationDirectory.Path);
var now = new DateTimeOffset(2026, 7, 12, 1, 2, 3, TimeSpan.Zero);
using var importer = new LegacyManualOperatorDataImporter(
destinationDirectory.Path,
new FixedLegacyManualOperatorDataSource(sourceDirectory.Path, TimeSpan.Zero),
() => now);
var result = await importer.ImportAsync();
Assert.Equal(1, result.MarkerVersion);
Assert.Equal(15, result.NetSellRowCount);
Assert.Equal(3, result.ViItemCount);
Assert.Equal(now, result.ImportedAt);
Assert.All(DestinationFiles, name =>
Assert.True(File.Exists(Path.Combine(destinationDirectory.Path, name))));
var markerPath = Path.Combine(
destinationDirectory.Path,
LegacyManualOperatorDataImporter.MarkerFileName);
Assert.True(File.Exists(markerPath));
using var marker = JsonDocument.Parse(await File.ReadAllBytesAsync(markerPath));
Assert.Equal(1, marker.RootElement.GetProperty("schemaVersion").GetInt32());
Assert.Equal(result.SourceSha256, marker.RootElement.GetProperty("sourceSha256").GetString());
Assert.Matches(
"^[a-f0-9]{64}$",
marker.RootElement.GetProperty("productionSha256").GetString()!);
Assert.Equal(15, marker.RootElement.GetProperty("netSellRowCount").GetInt32());
Assert.Equal(3, marker.RootElement.GetProperty("viItemCount").GetInt32());
Assert.False(File.Exists(Path.Combine(
destinationDirectory.Path,
LegacyManualOperatorDataImporter.IntentFileName)));
Assert.Equal(
LegacyManualOperatorDataRuntimeState.ReadyImportedData,
LegacyManualOperatorDataRuntimeGuard.Inspect(destinationDirectory.Path).State);
using var netStore = new S5025TrustedManualFileStore(destinationDirectory.Path);
using var viStore = new ViTrustedManualFileStore(destinationDirectory.Path);
Assert.Equal(Rows(), await netStore.ReadAsync(S5025ManualAudience.Individual));
Assert.Equal(3, (await viStore.ReadAsync()).Count);
var serializedResult = JsonSerializer.Serialize(result);
var serializedMarker = await File.ReadAllTextAsync(markerPath);
Assert.DoesNotContain(sourceDirectory.Path, serializedResult, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain(sourceDirectory.Path, serializedMarker, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("source\\repos", serializedMarker, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task Empty_vi_file_keeps_its_distinct_zero_length_contract_after_import_and_restart()
{
using var destinationDirectory = new TemporaryDirectory();
PreparePrivate(destinationDirectory.Path);
var snapshot = new LegacyManualOperatorSourceSnapshot(
Rows(),
Rows(),
Rows(),
Array.Empty<LegacyManualViItem>(),
new string('c', 64));
using var importer = new LegacyManualOperatorDataImporter(
destinationDirectory.Path,
new CountingSource(snapshot));
var result = await importer.ImportAsync();
Assert.Equal(0, result.ViItemCount);
Assert.Equal(
0,
new FileInfo(Path.Combine(destinationDirectory.Path, "VI발동.dat")).Length);
Assert.Equal(
LegacyManualOperatorDataRuntimeState.ReadyImportedData,
LegacyManualOperatorDataRuntimeGuard.Inspect(destinationDirectory.Path).State);
using var viStore = new ViTrustedManualFileStore(destinationDirectory.Path);
Assert.Empty(await viStore.ReadAsync());
}
[Theory]
[InlineData("개인.dat")]
[InlineData("외국인.dat")]
[InlineData("기관.dat")]
[InlineData("VI발동.dat")]
public async Task Any_existing_private_data_file_blocks_the_whole_import_without_overwrite(
string existingFile)
{
using var destinationDirectory = new TemporaryDirectory();
PreparePrivate(destinationDirectory.Path);
var path = Path.Combine(destinationDirectory.Path, existingFile);
var original = new byte[] { 1, 2, 3, 4 };
await File.WriteAllBytesAsync(path, original);
var source = new CountingSource(Snapshot());
using var importer = new LegacyManualOperatorDataImporter(
destinationDirectory.Path,
source);
var error = await Assert.ThrowsAsync<LegacyManualOperatorImportException>(
() => importer.ImportAsync());
Assert.Equal(LegacyManualOperatorImportFailure.DestinationNotEmpty, error.Failure);
Assert.False(error.OutcomeUnknown);
Assert.Equal(0, source.ReadCount);
Assert.Equal(original, await File.ReadAllBytesAsync(path));
Assert.Single(Directory.EnumerateFileSystemEntries(destinationDirectory.Path));
}
[Fact]
public async Task Marker_blocks_a_second_import_before_the_source_is_read_again()
{
using var destinationDirectory = new TemporaryDirectory();
PreparePrivate(destinationDirectory.Path);
var source = new CountingSource(Snapshot());
using var importer = new LegacyManualOperatorDataImporter(
destinationDirectory.Path,
source);
_ = await importer.ImportAsync();
var error = await Assert.ThrowsAsync<LegacyManualOperatorImportException>(
() => importer.ImportAsync());
Assert.Equal(LegacyManualOperatorImportFailure.AlreadyImported, error.Failure);
Assert.False(error.OutcomeUnknown);
Assert.Equal(1, source.ReadCount);
}
[Fact]
public async Task Preexisting_intent_and_partial_data_fail_closed_before_source_or_store_use()
{
using var destinationDirectory = new TemporaryDirectory();
PreparePrivate(destinationDirectory.Path);
var intentPath = Path.Combine(
destinationDirectory.Path,
LegacyManualOperatorDataImporter.IntentFileName);
await File.WriteAllTextAsync(intentPath, "{}");
var partialPath = Path.Combine(destinationDirectory.Path, DestinationFiles[0]);
var partialBytes = LegacyManualOperatorDataCodec.SerializeNetSell(Rows());
await File.WriteAllBytesAsync(partialPath, partialBytes);
var inspection = LegacyManualOperatorDataRuntimeGuard.Inspect(
destinationDirectory.Path);
var source = new CountingSource(Snapshot());
using var importer = new LegacyManualOperatorDataImporter(
destinationDirectory.Path,
source);
var error = await Assert.ThrowsAsync<LegacyManualOperatorImportException>(
() => importer.ImportAsync());
Assert.Equal(LegacyManualOperatorDataRuntimeState.InterruptedImport, inspection.State);
Assert.False(inspection.IsReady);
Assert.Equal(LegacyManualOperatorImportFailure.InterruptedImport, error.Failure);
Assert.True(error.OutcomeUnknown);
Assert.Equal(0, source.ReadCount);
Assert.Equal(partialBytes, await File.ReadAllBytesAsync(partialPath));
Assert.Equal("{}", await File.ReadAllTextAsync(intentPath));
}
[Fact]
public async Task Existing_legacy_files_without_marker_or_intent_remain_runtime_eligible()
{
using var destinationDirectory = new TemporaryDirectory();
PreparePrivate(destinationDirectory.Path);
await File.WriteAllBytesAsync(
Path.Combine(destinationDirectory.Path, DestinationFiles[0]),
LegacyManualOperatorDataCodec.SerializeNetSell(Rows()));
var inspection = LegacyManualOperatorDataRuntimeGuard.Inspect(
destinationDirectory.Path);
Assert.True(inspection.IsReady);
Assert.Equal(
LegacyManualOperatorDataRuntimeState.ReadyLegacyData,
inspection.State);
}
[Theory]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
[InlineData(4)]
public async Task Failure_after_each_data_move_rolls_back_data_and_intent(
int failAfterMove)
{
using var destinationDirectory = new TemporaryDirectory();
PreparePrivate(destinationDirectory.Path);
using var importer = new LegacyManualOperatorDataImporter(
destinationDirectory.Path,
new CountingSource(Snapshot()),
utcNow: null,
afterDataFileMoved: movedCount =>
{
if (movedCount == failAfterMove)
{
throw new IOException("injected move failure");
}
});
var error = await Assert.ThrowsAsync<LegacyManualOperatorImportException>(
() => importer.ImportAsync());
Assert.Equal(LegacyManualOperatorImportFailure.ImportFailed, error.Failure);
Assert.False(error.OutcomeUnknown);
Assert.All(DestinationFiles, name =>
Assert.False(File.Exists(Path.Combine(destinationDirectory.Path, name))));
Assert.False(File.Exists(Path.Combine(
destinationDirectory.Path,
LegacyManualOperatorDataImporter.MarkerFileName)));
Assert.False(File.Exists(Path.Combine(
destinationDirectory.Path,
LegacyManualOperatorDataImporter.IntentFileName)));
}
[Theory]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
[InlineData(4)]
public async Task Abrupt_termination_after_each_move_leaves_intent_and_blocks_all_reuse(
int crashAfterMove)
{
using var destinationDirectory = new TemporaryDirectory();
PreparePrivate(destinationDirectory.Path);
using (var importer = new LegacyManualOperatorDataImporter(
destinationDirectory.Path,
new CountingSource(Snapshot()),
utcNow: null,
afterDataFileMoved: movedCount =>
{
if (movedCount == crashAfterMove)
{
throw new LegacyManualOperatorImportCrashSimulationException();
}
}))
{
await Assert.ThrowsAsync<LegacyManualOperatorImportCrashSimulationException>(
() => importer.ImportAsync());
}
Assert.True(File.Exists(Path.Combine(
destinationDirectory.Path,
LegacyManualOperatorDataImporter.IntentFileName)));
Assert.Equal(
crashAfterMove,
DestinationFiles.Count(name => File.Exists(Path.Combine(
destinationDirectory.Path,
name))));
Assert.Equal(
LegacyManualOperatorDataRuntimeState.InterruptedImport,
LegacyManualOperatorDataRuntimeGuard.Inspect(destinationDirectory.Path).State);
var secondSource = new CountingSource(Snapshot());
using var secondImporter = new LegacyManualOperatorDataImporter(
destinationDirectory.Path,
secondSource);
var error = await Assert.ThrowsAsync<LegacyManualOperatorImportException>(
() => secondImporter.ImportAsync());
Assert.Equal(LegacyManualOperatorImportFailure.InterruptedImport, error.Failure);
Assert.True(error.OutcomeUnknown);
Assert.Equal(0, secondSource.ReadCount);
}
[Fact]
public async Task Abrupt_termination_after_completed_marker_keeps_stale_intent_fail_closed()
{
using var destinationDirectory = new TemporaryDirectory();
PreparePrivate(destinationDirectory.Path);
using (var importer = new LegacyManualOperatorDataImporter(
destinationDirectory.Path,
new CountingSource(Snapshot()),
utcNow: null,
afterDataFileMoved: null,
afterMarkerCommitted: () =>
throw new LegacyManualOperatorImportCrashSimulationException()))
{
await Assert.ThrowsAsync<LegacyManualOperatorImportCrashSimulationException>(
() => importer.ImportAsync());
}
Assert.All(DestinationFiles, name =>
Assert.True(File.Exists(Path.Combine(destinationDirectory.Path, name))));
Assert.True(File.Exists(Path.Combine(
destinationDirectory.Path,
LegacyManualOperatorDataImporter.MarkerFileName)));
Assert.True(File.Exists(Path.Combine(
destinationDirectory.Path,
LegacyManualOperatorDataImporter.IntentFileName)));
var inspection = LegacyManualOperatorDataRuntimeGuard.Inspect(
destinationDirectory.Path);
Assert.False(inspection.IsReady);
Assert.Equal(LegacyManualOperatorDataRuntimeState.InterruptedImport, inspection.State);
}
[Fact]
public async Task Completed_marker_with_missing_or_modified_data_is_rejected()
{
using var missingDirectory = new TemporaryDirectory();
PreparePrivate(missingDirectory.Path);
using (var importer = new LegacyManualOperatorDataImporter(
missingDirectory.Path,
new CountingSource(Snapshot())))
{
_ = await importer.ImportAsync();
}
File.Delete(Path.Combine(missingDirectory.Path, DestinationFiles[0]));
Assert.Equal(
LegacyManualOperatorDataRuntimeState.InvalidImportedState,
LegacyManualOperatorDataRuntimeGuard.Inspect(missingDirectory.Path).State);
using var modifiedDirectory = new TemporaryDirectory();
PreparePrivate(modifiedDirectory.Path);
using (var importer = new LegacyManualOperatorDataImporter(
modifiedDirectory.Path,
new CountingSource(Snapshot())))
{
_ = await importer.ImportAsync();
}
await File.AppendAllTextAsync(
Path.Combine(modifiedDirectory.Path, DestinationFiles[0]),
"tamper");
Assert.Equal(
LegacyManualOperatorDataRuntimeState.InvalidImportedState,
LegacyManualOperatorDataRuntimeGuard.Inspect(modifiedDirectory.Path).State);
}
[Fact]
public async Task Completed_marker_allows_later_verified_operator_edits()
{
using var destinationDirectory = new TemporaryDirectory();
PreparePrivate(destinationDirectory.Path);
using (var importer = new LegacyManualOperatorDataImporter(
destinationDirectory.Path,
new CountingSource(Snapshot())))
{
_ = await importer.ImportAsync();
}
var editedRows = Rows().ToArray();
editedRows[0] = editedRows[0] with { LeftAmount = "9,999" };
using (var store = new S5025TrustedManualFileStore(destinationDirectory.Path))
{
await store.WriteAsync(S5025ManualAudience.Individual, editedRows);
}
Assert.Equal(
LegacyManualOperatorDataRuntimeState.ReadyImportedData,
LegacyManualOperatorDataRuntimeGuard.Inspect(destinationDirectory.Path).State);
}
[Fact]
public async Task Commit_failure_rolls_back_every_file_created_by_the_operation_without_retry()
{
using var destinationDirectory = new TemporaryDirectory();
PreparePrivate(destinationDirectory.Path);
var source = new CountingSource(Snapshot());
using var importer = new LegacyManualOperatorDataImporter(
destinationDirectory.Path,
source,
utcNow: null,
afterDataFileMoved: movedCount =>
{
if (movedCount == 1)
{
Directory.CreateDirectory(Path.Combine(destinationDirectory.Path, "외국인.dat"));
}
});
var error = await Assert.ThrowsAsync<LegacyManualOperatorImportException>(
() => importer.ImportAsync());
Assert.Equal(LegacyManualOperatorImportFailure.ImportFailed, error.Failure);
Assert.False(error.OutcomeUnknown);
Assert.Equal(1, source.ReadCount);
Assert.False(File.Exists(Path.Combine(destinationDirectory.Path, "개인.dat")));
Assert.False(File.Exists(Path.Combine(destinationDirectory.Path, "기관.dat")));
Assert.False(File.Exists(Path.Combine(destinationDirectory.Path, "VI발동.dat")));
Assert.False(File.Exists(Path.Combine(
destinationDirectory.Path,
LegacyManualOperatorDataImporter.MarkerFileName)));
Assert.Empty(Directory.EnumerateFiles(destinationDirectory.Path, "*.tmp"));
}
[Fact]
public async Task Unsafe_rollback_is_reported_as_outcome_unknown_and_is_never_retried()
{
using var destinationDirectory = new TemporaryDirectory();
PreparePrivate(destinationDirectory.Path);
var source = new CountingSource(Snapshot());
using var importer = new LegacyManualOperatorDataImporter(
destinationDirectory.Path,
source,
utcNow: null,
afterDataFileMoved: movedCount =>
{
if (movedCount != 1)
{
return;
}
File.WriteAllText(Path.Combine(destinationDirectory.Path, "개인.dat"), "tampered");
Directory.CreateDirectory(Path.Combine(destinationDirectory.Path, "외국인.dat"));
});
var error = await Assert.ThrowsAsync<LegacyManualOperatorImportException>(
() => importer.ImportAsync());
Assert.Equal(LegacyManualOperatorImportFailure.RollbackFailed, error.Failure);
Assert.True(error.OutcomeUnknown);
Assert.Equal(1, source.ReadCount);
Assert.True(File.Exists(Path.Combine(destinationDirectory.Path, "개인.dat")));
Assert.False(File.Exists(Path.Combine(
destinationDirectory.Path,
LegacyManualOperatorDataImporter.MarkerFileName)));
Assert.True(File.Exists(Path.Combine(
destinationDirectory.Path,
LegacyManualOperatorDataImporter.IntentFileName)));
Assert.Equal(
LegacyManualOperatorDataRuntimeState.InterruptedImport,
LegacyManualOperatorDataRuntimeGuard.Inspect(destinationDirectory.Path).State);
}
[Fact]
public async Task Invalid_source_format_creates_no_private_file_and_does_not_disclose_the_path()
{
using var sourceDirectory = new TemporaryDirectory();
using var destinationDirectory = new TemporaryDirectory();
WriteSourceFiles(sourceDirectory.Path);
await File.WriteAllTextAsync(Path.Combine(sourceDirectory.Path, "기관.dat"), "invalid");
PreparePrivate(destinationDirectory.Path);
using var importer = new LegacyManualOperatorDataImporter(
destinationDirectory.Path,
new FixedLegacyManualOperatorDataSource(sourceDirectory.Path, TimeSpan.Zero));
var error = await Assert.ThrowsAsync<LegacyManualOperatorImportException>(
() => importer.ImportAsync());
Assert.Equal(LegacyManualOperatorImportFailure.SourceInvalid, error.Failure);
Assert.DoesNotContain(sourceDirectory.Path, error.Message, StringComparison.OrdinalIgnoreCase);
Assert.Empty(Directory.EnumerateFileSystemEntries(destinationDirectory.Path));
}
private static void WriteSourceFiles(string directory)
{
foreach (var fileName in DestinationFiles.Take(3))
{
File.WriteAllBytes(
Path.Combine(directory, fileName),
LegacyManualOperatorDataCodec.SerializeNetSell(Rows()));
}
File.WriteAllBytes(
Path.Combine(directory, "VI발동.dat"),
LegacyManualOperatorDataCodec.SerializeVi(ViItems()));
}
private static LegacyManualOperatorSourceSnapshot Snapshot() => new(
Rows(),
Rows(),
Rows(),
ViItems(),
new string('a', 64));
private static IReadOnlyList<S5025TrustedManualRow> Rows() =>
[
new("삼성전자", "1,000", "SK하이닉스", "-2,000"),
new("현대차", "2", "기아", "-2"),
new("NAVER", "3", "카카오", "-3"),
new("LG화학", "4", "포스코", "-4"),
new("한화", "5", "셀트리온", "-5")
];
private static IReadOnlyList<LegacyManualViItem> ViItems() =>
[
new("PKR7005930003", "삼성전자"),
new("PKR7016360000", "삼성증권"),
new("PKR7005930003", "삼성전자")
];
private static void PreparePrivate(string path) =>
TrustedManualDirectory.PreparePrivateWritableDirectory(path);
private sealed class CountingSource : ILegacyManualOperatorDataSource
{
private readonly LegacyManualOperatorSourceSnapshot _snapshot;
public CountingSource(LegacyManualOperatorSourceSnapshot snapshot)
{
_snapshot = snapshot;
}
public int ReadCount { get; private set; }
public Task<LegacyManualOperatorSourceSnapshot> ReadAsync(
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ReadCount++;
return Task.FromResult(_snapshot);
}
}
private sealed class TemporaryDirectory : IDisposable
{
public TemporaryDirectory()
{
Path = System.IO.Path.Combine(
System.IO.Path.GetTempPath(),
Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(Path);
}
public string Path { get; }
public void Dispose()
{
try
{
Directory.Delete(Path, recursive: true);
}
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException)
{
// Cleanup must not hide the test result.
}
}
}
}