using System.Text; using MBN_STOCK_WEBVIEW.Core.Playout.Scenes; using MBN_STOCK_WEBVIEW.Infrastructure; namespace MBN_STOCK_WEBVIEW.Infrastructure.Tests; public sealed class S5025TrustedManualFileStoreTests { [Theory] [InlineData(S5025ManualAudience.Individual, "개인.dat")] [InlineData(S5025ManualAudience.Foreign, "외국인.dat")] [InlineData(S5025ManualAudience.Institution, "기관.dat")] public async Task WriteAsync_round_trips_the_closed_audience_file( S5025ManualAudience audience, string expectedFileName) { using var directory = new TemporaryDirectory(); using var store = CreateStore(directory.Path); await store.WriteAsync(audience, Rows()); Assert.True(File.Exists(Path.Combine(directory.Path, expectedFileName))); var actual = await store.ReadAsync(audience); Assert.Equal(Rows(), actual); Assert.Empty(Directory.EnumerateFiles(directory.Path, "*.tmp")); } [Fact] public async Task WriteAsync_preserves_the_legacy_cp949_terminal_record() { using var directory = new TemporaryDirectory(); using var store = CreateStore(directory.Path); await store.WriteAsync(S5025ManualAudience.Individual, Rows()); Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); var cp949 = Encoding.GetEncoding(949); var bytes = await File.ReadAllBytesAsync(Path.Combine(directory.Path, "개인.dat")); var text = cp949.GetString(bytes); Assert.EndsWith("\r\n\r\n", text, StringComparison.Ordinal); Assert.Equal(5, text.Split("\r\n", StringSplitOptions.None).Length - 2); } [Theory] [InlineData(0)] [InlineData(4)] [InlineData(6)] public async Task WriteAsync_requires_exactly_five_rows(int rowCount) { using var directory = new TemporaryDirectory(); using var store = CreateStore(directory.Path); await Assert.ThrowsAsync(() => store.WriteAsync( S5025ManualAudience.Individual, Rows().Take(rowCount).Concat(Enumerable.Repeat(Rows()[0], Math.Max(0, rowCount - 5))) .Take(rowCount) .ToArray())); } [Theory] [InlineData("bad^name")] [InlineData("bad\nname")] public async Task WriteAsync_rejects_delimiter_and_control_characters(string value) { using var directory = new TemporaryDirectory(); using var store = CreateStore(directory.Path); var rows = Rows().ToArray(); rows[0] = rows[0] with { LeftName = value }; await Assert.ThrowsAsync(() => store.WriteAsync(S5025ManualAudience.Individual, rows)); } [Fact] public async Task WriteAsync_rejects_text_that_cp949_cannot_encode() { using var directory = new TemporaryDirectory(); using var store = CreateStore(directory.Path); var rows = Rows().ToArray(); rows[0] = rows[0] with { LeftName = "😀" }; await Assert.ThrowsAsync(() => store.WriteAsync(S5025ManualAudience.Individual, rows)); } [Fact] public async Task WriteAsync_honors_pre_cancelled_token_without_creating_a_file() { using var directory = new TemporaryDirectory(); using var store = CreateStore(directory.Path); using var cancellation = new CancellationTokenSource(); cancellation.Cancel(); await Assert.ThrowsAnyAsync(() => store.WriteAsync(S5025ManualAudience.Individual, Rows(), cancellation.Token)); Assert.Empty(Directory.EnumerateFiles(directory.Path)); } [Fact] public async Task WriteAsync_rejects_unknown_audience_before_creating_a_file() { using var directory = new TemporaryDirectory(); using var store = CreateStore(directory.Path); await Assert.ThrowsAsync(() => store.WriteAsync((S5025ManualAudience)99, Rows())); Assert.Empty(Directory.EnumerateFiles(directory.Path)); } [Fact] public async Task WriteAsync_rejects_an_existing_reparse_destination() { using var directory = new TemporaryDirectory(); using var outside = new TemporaryDirectory(); var target = Path.Combine(outside.Path, "outside.dat"); await File.WriteAllTextAsync(target, "outside"); var link = Path.Combine(directory.Path, "개인.dat"); if (!TryCreateFileSymbolicLink(link, target)) { return; } using var store = CreateStore(directory.Path); await Assert.ThrowsAsync(() => store.WriteAsync(S5025ManualAudience.Individual, Rows())); Assert.Equal("outside", await File.ReadAllTextAsync(target)); } private static IReadOnlyList Rows() => [ new("삼성전자", "1,234", "SK하이닉스", "-987"), new("현대차", "2", "기아", "-2"), new("NAVER", "3", "카카오", "-3"), new("LG화학", "4", "포스코", "-4"), new("한화", "5", "셀트리온", "-5") ]; private static S5025TrustedManualFileStore CreateStore(string directory) { TrustedManualDirectory.PreparePrivateWritableDirectory(directory); return new S5025TrustedManualFileStore(directory); } private static bool TryCreateFileSymbolicLink(string link, string target) { try { File.CreateSymbolicLink(link, target); return true; } catch (Exception exception) when (exception is UnauthorizedAccessException or IOException or PlatformNotSupportedException) { return false; } } 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. } } } }