332 lines
11 KiB
C#
332 lines
11 KiB
C#
using System.Text;
|
|
using MBN_STOCK_WEBVIEW.Core.Playout.Scenes;
|
|
using MBN_STOCK_WEBVIEW.Infrastructure;
|
|
|
|
namespace MBN_STOCK_WEBVIEW.Infrastructure.Tests;
|
|
|
|
public sealed class S5025TrustedManualFileDataSourceTests
|
|
{
|
|
private static readonly Encoding Cp949 = CreateCp949Encoding();
|
|
|
|
[Theory]
|
|
[InlineData(S5025ManualAudience.Individual, "개인.dat")]
|
|
[InlineData(S5025ManualAudience.Foreign, "외국인.dat")]
|
|
[InlineData(S5025ManualAudience.Institution, "기관.dat")]
|
|
public async Task ReadAsync_maps_closed_audience_to_fixed_cp949_file(
|
|
S5025ManualAudience audience,
|
|
string expectedFileName)
|
|
{
|
|
using var directory = new TemporaryDirectory();
|
|
WriteValidFile(Path.Combine(directory.Path, expectedFileName));
|
|
var source = new S5025TrustedManualFileDataSource(directory.Path);
|
|
|
|
var rows = await source.ReadAsync(audience);
|
|
|
|
Assert.Equal(5, rows.Count);
|
|
Assert.Equal("삼성전자", rows[0].LeftName);
|
|
Assert.Equal("1,234", rows[0].LeftAmount);
|
|
Assert.Equal("SK하이닉스", rows[0].RightName);
|
|
Assert.Equal("-987", rows[0].RightAmount);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateFromEnvironment_uses_only_documented_directory_setting()
|
|
{
|
|
using var directory = new TemporaryDirectory();
|
|
WriteValidFile(Path.Combine(directory.Path, "개인.dat"));
|
|
var requestedNames = new List<string>();
|
|
var source = S5025TrustedManualFileDataSource.CreateFromEnvironment(name =>
|
|
{
|
|
requestedNames.Add(name);
|
|
return directory.Path;
|
|
});
|
|
|
|
var rows = await source.ReadAsync(S5025ManualAudience.Individual);
|
|
|
|
Assert.Equal(
|
|
S5025TrustedManualFileDataSource.DirectoryEnvironmentVariable,
|
|
Assert.Single(requestedNames));
|
|
Assert.Equal(5, rows.Count);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(null)]
|
|
[InlineData("")]
|
|
[InlineData("relative-data")]
|
|
public void Constructor_rejects_missing_or_non_absolute_directory(string? path)
|
|
{
|
|
Assert.Throws<S5025TrustedManualDataException>(() =>
|
|
new S5025TrustedManualFileDataSource(path!));
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_rejects_nonexistent_absolute_directory()
|
|
{
|
|
var missing = Path.Combine(
|
|
Path.GetTempPath(),
|
|
Guid.NewGuid().ToString("N"),
|
|
"missing");
|
|
|
|
Assert.Throws<S5025TrustedManualDataException>(() =>
|
|
new S5025TrustedManualFileDataSource(missing));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(S5025ManualAudience.Individual)]
|
|
[InlineData(S5025ManualAudience.Foreign)]
|
|
[InlineData(S5025ManualAudience.Institution)]
|
|
public async Task ReadAsync_does_not_fall_back_when_fixed_file_is_missing(
|
|
S5025ManualAudience audience)
|
|
{
|
|
using var directory = new TemporaryDirectory();
|
|
var source = new S5025TrustedManualFileDataSource(directory.Path);
|
|
|
|
await Assert.ThrowsAsync<S5025TrustedManualDataException>(() =>
|
|
source.ReadAsync(audience));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReadAsync_rejects_unknown_audience_before_any_file_lookup()
|
|
{
|
|
using var directory = new TemporaryDirectory();
|
|
var source = new S5025TrustedManualFileDataSource(directory.Path);
|
|
|
|
await Assert.ThrowsAsync<S5025TrustedManualDataException>(() =>
|
|
source.ReadAsync((S5025ManualAudience)99));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(
|
|
"L1^1^R1^2\r\nL2^1^R2^2\r\nL3^1^R3^2\r\nL4^1^R4^2",
|
|
"four rows")]
|
|
[InlineData(
|
|
"L1^1^R1^2\r\nL2^1^R2^2\r\nL3^1^R3^2\r\nL4^1^R4^2\r\nL5^1^R5^2\r\nL6^1^R6^2",
|
|
"six rows")]
|
|
[InlineData(
|
|
"L1^1^R1\r\nL2^1^R2^2\r\nL3^1^R3^2\r\nL4^1^R4^2\r\nL5^1^R5^2",
|
|
"three columns")]
|
|
[InlineData(
|
|
"L1^1^R1^2^EXTRA\r\nL2^1^R2^2\r\nL3^1^R3^2\r\nL4^1^R4^2\r\nL5^1^R5^2",
|
|
"five columns")]
|
|
public async Task ReadAsync_requires_exactly_five_rows_and_four_columns(
|
|
string content,
|
|
string _)
|
|
{
|
|
using var directory = new TemporaryDirectory();
|
|
await File.WriteAllBytesAsync(
|
|
Path.Combine(directory.Path, "개인.dat"),
|
|
Cp949.GetBytes(content + "\r\n\r\n"));
|
|
var source = new S5025TrustedManualFileDataSource(directory.Path);
|
|
|
|
await Assert.ThrowsAsync<S5025TrustedManualDataException>(() =>
|
|
source.ReadAsync(S5025ManualAudience.Individual));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReadAsync_rejects_missing_empty_terminal_record()
|
|
{
|
|
using var directory = new TemporaryDirectory();
|
|
await File.WriteAllBytesAsync(
|
|
Path.Combine(directory.Path, "개인.dat"),
|
|
Cp949.GetBytes(ValidContent() + "\r\n"));
|
|
var source = new S5025TrustedManualFileDataSource(directory.Path);
|
|
|
|
await Assert.ThrowsAsync<S5025TrustedManualDataException>(() =>
|
|
source.ReadAsync(S5025ManualAudience.Individual));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReadAsync_accepts_the_original_single_empty_terminal_record()
|
|
{
|
|
using var directory = new TemporaryDirectory();
|
|
var content = ValidContent() + "\r\n\r\n";
|
|
await File.WriteAllBytesAsync(
|
|
Path.Combine(directory.Path, "개인.dat"),
|
|
Cp949.GetBytes(content));
|
|
var source = new S5025TrustedManualFileDataSource(directory.Path);
|
|
|
|
var rows = await source.ReadAsync(S5025ManualAudience.Individual);
|
|
|
|
Assert.Equal(5, rows.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReadAsync_rejects_multiple_empty_terminal_records()
|
|
{
|
|
using var directory = new TemporaryDirectory();
|
|
var content = ValidContent() + "\r\n\r\n\r\n";
|
|
await File.WriteAllBytesAsync(
|
|
Path.Combine(directory.Path, "개인.dat"),
|
|
Cp949.GetBytes(content));
|
|
var source = new S5025TrustedManualFileDataSource(directory.Path);
|
|
|
|
await Assert.ThrowsAsync<S5025TrustedManualDataException>(() =>
|
|
source.ReadAsync(S5025ManualAudience.Individual));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReadAsync_rejects_invalid_cp949_and_unicode_bom_files()
|
|
{
|
|
using var invalidDirectory = new TemporaryDirectory();
|
|
await File.WriteAllBytesAsync(
|
|
Path.Combine(invalidDirectory.Path, "개인.dat"),
|
|
[0x81]);
|
|
var invalidSource = new S5025TrustedManualFileDataSource(invalidDirectory.Path);
|
|
await Assert.ThrowsAsync<S5025TrustedManualDataException>(() =>
|
|
invalidSource.ReadAsync(S5025ManualAudience.Individual));
|
|
|
|
using var bomDirectory = new TemporaryDirectory();
|
|
var content = ValidContent();
|
|
await File.WriteAllBytesAsync(
|
|
Path.Combine(bomDirectory.Path, "개인.dat"),
|
|
Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(content)).ToArray());
|
|
var bomSource = new S5025TrustedManualFileDataSource(bomDirectory.Path);
|
|
await Assert.ThrowsAsync<S5025TrustedManualDataException>(() =>
|
|
bomSource.ReadAsync(S5025ManualAudience.Individual));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReadAsync_rejects_oversized_file_before_decoding()
|
|
{
|
|
using var directory = new TemporaryDirectory();
|
|
await File.WriteAllBytesAsync(
|
|
Path.Combine(directory.Path, "개인.dat"),
|
|
new byte[S5025TrustedManualFileDataSource.MaximumFileSizeBytes + 1]);
|
|
var source = new S5025TrustedManualFileDataSource(directory.Path);
|
|
|
|
await Assert.ThrowsAsync<S5025TrustedManualDataException>(() =>
|
|
source.ReadAsync(S5025ManualAudience.Individual));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReadAsync_rejects_reparse_file_in_trusted_directory()
|
|
{
|
|
using var directory = new TemporaryDirectory();
|
|
using var outside = new TemporaryDirectory();
|
|
var target = Path.Combine(outside.Path, "outside.dat");
|
|
WriteValidFile(target);
|
|
var link = Path.Combine(directory.Path, "개인.dat");
|
|
if (!TryCreateFileSymbolicLink(link, target))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var source = new S5025TrustedManualFileDataSource(directory.Path);
|
|
|
|
await Assert.ThrowsAsync<S5025TrustedManualDataException>(() =>
|
|
source.ReadAsync(S5025ManualAudience.Individual));
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_rejects_reparse_directory_or_ancestor()
|
|
{
|
|
using var parent = new TemporaryDirectory();
|
|
using var outside = new TemporaryDirectory();
|
|
var link = Path.Combine(parent.Path, "linked-data");
|
|
if (!TryCreateDirectorySymbolicLink(link, outside.Path))
|
|
{
|
|
return;
|
|
}
|
|
|
|
Assert.Throws<S5025TrustedManualDataException>(() =>
|
|
new S5025TrustedManualFileDataSource(link));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReadAsync_honors_pre_cancelled_token()
|
|
{
|
|
using var directory = new TemporaryDirectory();
|
|
WriteValidFile(Path.Combine(directory.Path, "개인.dat"));
|
|
var source = new S5025TrustedManualFileDataSource(directory.Path);
|
|
using var cancellation = new CancellationTokenSource();
|
|
cancellation.Cancel();
|
|
|
|
await Assert.ThrowsAnyAsync<OperationCanceledException>(() =>
|
|
source.ReadAsync(S5025ManualAudience.Individual, cancellation.Token));
|
|
}
|
|
|
|
private static void WriteValidFile(string path)
|
|
{
|
|
var content = ValidContent() + "\r\n\r\n";
|
|
File.WriteAllBytes(path, Cp949.GetBytes(content));
|
|
}
|
|
|
|
private static string ValidContent() => string.Join(
|
|
"\r\n",
|
|
"삼성전자^1,234^SK하이닉스^-987",
|
|
"현대차^2^기아^-2",
|
|
"NAVER^3^카카오^-3",
|
|
"LG화학^4^셀트리온^-4",
|
|
"POSCO홀딩스^5^한화에어로스페이스^-5");
|
|
|
|
private static Encoding CreateCp949Encoding()
|
|
{
|
|
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
|
return Encoding.GetEncoding(
|
|
949,
|
|
EncoderFallback.ExceptionFallback,
|
|
DecoderFallback.ExceptionFallback);
|
|
}
|
|
|
|
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 static bool TryCreateDirectorySymbolicLink(string link, string target)
|
|
{
|
|
try
|
|
{
|
|
Directory.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 (IOException)
|
|
{
|
|
// A failed cleanup must not hide the behavior under test.
|
|
}
|
|
catch (UnauthorizedAccessException)
|
|
{
|
|
// A failed cleanup must not hide the behavior under test.
|
|
}
|
|
}
|
|
}
|
|
}
|