using System.Text; using MBN_STOCK_WEBVIEW.Infrastructure; namespace MBN_STOCK_WEBVIEW.Infrastructure.Tests; public sealed class ViTrustedManualFileStoreTests { [Fact] public async Task ReadAsync_imports_the_original_nine_column_cp949_shape() { using var directory = new TemporaryDirectory(); Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); var cp949 = Encoding.GetEncoding(949); await File.WriteAllBytesAsync( Path.Combine(directory.Path, "VI발동.dat"), cp949.GetBytes( "PKR7005930003^삼성전자^^^^^^^\r\n" + "PKR7016360000^삼성증권^^^^^^^\r\n")); using var store = CreateStore(directory.Path); var actual = await store.ReadAsync(); Assert.Equal( [ new ViTrustedManualItem("PKR7005930003", "삼성전자"), new ViTrustedManualItem("PKR7016360000", "삼성증권") ], actual); } [Fact] public async Task WriteAsync_round_trips_order_duplicates_and_version() { using var directory = new TemporaryDirectory(); using var store = CreateStore(directory.Path); ViTrustedManualItem[] expected = [ new("005930", "삼성전자"), new("016360", "삼성증권"), new("005930", "삼성전자") ]; var write = await store.WriteAsync(expected); Assert.True(write.Persisted); Assert.Matches("^[a-f0-9]{64}$", write.Snapshot.RowVersion); Assert.Equal(expected, await store.ReadAsync()); Assert.Empty(Directory.EnumerateFiles(directory.Path, "*.tmp")); } [Fact] public async Task Empty_list_is_an_original_compatible_no_op() { using var directory = new TemporaryDirectory(); using var store = CreateStore(directory.Path); var firstNoOp = await store.WriteAsync(Array.Empty()); Assert.False(firstNoOp.Persisted); Assert.False(File.Exists(Path.Combine(directory.Path, "VI발동.dat"))); Assert.Empty(await store.ReadAsync()); var expected = new[] { new ViTrustedManualItem("005930", "삼성전자") }; var written = await store.WriteAsync(expected); var secondNoOp = await store.WriteAsync(Array.Empty()); Assert.False(secondNoOp.Persisted); Assert.Equal(written.Snapshot.RowVersion, secondNoOp.Snapshot.RowVersion); Assert.Equal(expected, await store.ReadAsync()); } [Fact] public async Task Missing_file_is_an_empty_versioned_first_run_list() { using var directory = new TemporaryDirectory(); using var store = CreateStore(directory.Path); var snapshot = await store.ReadSnapshotAsync(); Assert.Empty(snapshot.Items); Assert.Equal( "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", snapshot.RowVersion); } [Fact] public async Task ReadAsync_rejects_nonempty_legacy_extra_columns() { using var directory = new TemporaryDirectory(); await File.WriteAllTextAsync( Path.Combine(directory.Path, "VI발동.dat"), "005930^삼성전자^unexpected^^^^^^\r\n"); using var store = CreateStore(directory.Path); await Assert.ThrowsAsync(() => store.ReadAsync()); } [Theory] [InlineData("", "삼성전자")] [InlineData("005930", "")] [InlineData("005^930", "삼성전자")] [InlineData("005930", "삼성\n전자")] [InlineData(" 005930", "삼성전자")] [InlineData("005930", "삼성전자 ")] [InlineData("005930", "삼성,전자")] [InlineData("005930", "😀")] public async Task WriteAsync_rejects_invalid_or_non_cp949_values(string code, string name) { using var directory = new TemporaryDirectory(); using var store = CreateStore(directory.Path); await Assert.ThrowsAsync(() => store.WriteAsync([new ViTrustedManualItem(code, name)])); } [Fact] public async Task WriteAsync_caps_the_original_five_row_scene_at_twenty_pages() { using var directory = new TemporaryDirectory(); using var store = CreateStore(directory.Path); var items = Enumerable.Range(1, ViTrustedManualFileStore.MaximumItemCount + 1) .Select(index => new ViTrustedManualItem(index.ToString("D6"), $"종목{index}")) .ToArray(); await Assert.ThrowsAsync(() => store.WriteAsync(items)); } [Fact] public async Task WriteAsync_honors_pre_cancelled_token() { using var directory = new TemporaryDirectory(); using var store = CreateStore(directory.Path); using var cancellation = new CancellationTokenSource(); cancellation.Cancel(); await Assert.ThrowsAnyAsync(() => store.WriteAsync( [new ViTrustedManualItem("005930", "삼성전자")], cancellation.Token)); Assert.Empty(Directory.EnumerateFiles(directory.Path)); } [Fact] public async Task Canonical_snapshot_version_covers_all_twenty_pages() { using var directory = new TemporaryDirectory(); using var store = CreateStore(directory.Path); var items = Enumerable.Range(1, ViTrustedManualFileStore.MaximumItemCount) .Select(index => new ViTrustedManualItem( $"P{index:D6}", $"현실적인긴종목명테스트{index}")) .ToArray(); var written = await store.WriteAsync(items); var snapshot = await store.ReadSnapshotAsync(); Assert.True(written.Persisted); Assert.Equal(100, snapshot.Items.Count); Assert.Equal(written.Snapshot.RowVersion, snapshot.RowVersion); Assert.Matches("^[a-f0-9]{64}$", snapshot.RowVersion); } private static ViTrustedManualFileStore CreateStore(string directory) { TrustedManualDirectory.PreparePrivateWritableDirectory(directory); return new ViTrustedManualFileStore(directory); } 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. } } } }