using System.Text; using System.Text.Json; using MMoneyCoderSharp.Data; namespace MBN_STOCK_WEBVIEW.LegacyApplication.Tests; public sealed class LegacyOperatorSettingsTests { private static readonly Encoding Cp949 = CreateCp949Encoding(); [Fact] public void DefaultPath_IsTheBoundedLocalApplicationConfigurationPath() { var expected = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "MBN_STOCK_WEBVIEW", "Config", "runtime-folders.local.json"); Assert.Equal(expected, LegacyOperatorSettingsStore.DefaultPath); Assert.True(Path.IsPathFullyQualified(LegacyOperatorSettingsStore.DefaultPath)); } [Fact] public void SceneValidation_RequiresEveryActiveSceneAndBuiltInAssetAndAllowsMissingExternalVideo() { using var directory = new TemporaryDirectory(); var sceneDirectory = directory.CreateDirectory("Cuts"); WriteValidSceneFiles(sceneDirectory); var result = LegacyOperatorFolderValidator.Validate( LegacyOperatorSettingsFolderKind.Scene, sceneDirectory + Path.DirectorySeparatorChar); Assert.True(result.IsValid); Assert.Equal(Path.GetFullPath(sceneDirectory), result.Folder!.CanonicalPath); Assert.Equal("fixture-scene", File.ReadAllText( Path.Combine(result.Folder.CanonicalPath, "5001.t2s"))); Assert.Contains("Cuts", result.Folder.AbbreviatedPath); Assert.InRange( result.Folder.AbbreviatedPath.Length, 1, LegacyOperatorFolderValidator.DefaultDisplayCharacters); Assert.False(result.Folder.DatabaseIniDetected); Assert.Equal( LegacyOperatorFolderValidator.OptionalExternalAssetRelativePaths.Count, result.Folder.MissingOptionalExternalAssetCount); Assert.Null(result.WarningMessage); } [Fact] public void SceneValidation_BaselineOnlyIsRejectedInsteadOfBreakingOtherScenesAfterRestart() { using var directory = new TemporaryDirectory(); var sceneDirectory = directory.CreateDirectory("partial-Cuts"); File.WriteAllText(Path.Combine(sceneDirectory, "5001.t2s"), "baseline"); var result = LegacyOperatorFolderValidator.Validate( LegacyOperatorSettingsFolderKind.Scene, sceneDirectory); Assert.False(result.IsValid); Assert.Equal( LegacyOperatorFolderValidationFailure.RequiredFileMissing, result.Failure); Assert.Null(result.Folder); AssertPathFree(result.WarningMessage, sceneDirectory); } [Fact] public void SceneValidation_MissingOneActiveSceneOrBuiltInAssetIsRejected() { using var directory = new TemporaryDirectory(); var missingSceneDirectory = directory.CreateDirectory("missing-scene"); WriteValidSceneFiles(missingSceneDirectory); File.Delete(Path.Combine(missingSceneDirectory, "5079.t2s")); var missingScene = LegacyOperatorFolderValidator.Validate( LegacyOperatorSettingsFolderKind.Scene, missingSceneDirectory); var missingAssetDirectory = directory.CreateDirectory("missing-asset"); WriteValidSceneFiles(missingAssetDirectory); File.Delete(Path.Combine(missingAssetDirectory, "Images", "KRX.png")); var missingAsset = LegacyOperatorFolderValidator.Validate( LegacyOperatorSettingsFolderKind.Scene, missingAssetDirectory); Assert.False(missingScene.IsValid); Assert.Equal(LegacyOperatorFolderValidationFailure.RequiredFileMissing, missingScene.Failure); Assert.False(missingAsset.IsValid); Assert.Equal(LegacyOperatorFolderValidationFailure.RequiredFileMissing, missingAsset.Failure); } [Fact] public void SceneValidation_RejectsEmptyRequiredOrPresentOptionalFiles() { using var directory = new TemporaryDirectory(); var emptyRequiredDirectory = directory.CreateDirectory("empty-required"); WriteValidSceneFiles(emptyRequiredDirectory); File.WriteAllBytes(Path.Combine(emptyRequiredDirectory, "5076.t2s"), []); var emptyRequired = LegacyOperatorFolderValidator.Validate( LegacyOperatorSettingsFolderKind.Scene, emptyRequiredDirectory); var emptyOptionalDirectory = directory.CreateDirectory("empty-optional"); WriteValidSceneFiles(emptyOptionalDirectory); var optionalPath = Path.Combine(emptyOptionalDirectory, "Video", "큐브배경.vrv"); Directory.CreateDirectory(Path.GetDirectoryName(optionalPath)!); File.WriteAllBytes(optionalPath, []); var emptyOptional = LegacyOperatorFolderValidator.Validate( LegacyOperatorSettingsFolderKind.Scene, emptyOptionalDirectory); Assert.False(emptyRequired.IsValid); Assert.Equal(LegacyOperatorFolderValidationFailure.RequiredFileUnsafe, emptyRequired.Failure); Assert.False(emptyOptional.IsValid); Assert.Equal(LegacyOperatorFolderValidationFailure.RequiredFileUnsafe, emptyOptional.Failure); } [Fact] public void SceneValidationContract_MatchesTheReachableCutRequirementManifest() { var manifestPath = Path.Combine( FindRepositoryRoot(), "scripts", "LegacyCutRequirements.json"); using var document = JsonDocument.Parse(File.ReadAllBytes(manifestPath)); var root = document.RootElement; var manifestScenes = root.GetProperty("activeAliases") .EnumerateArray() .Select(value => string.Concat(value.GetString(), ".t2s")) .ToArray(); var manifestAssets = root.GetProperty("assets") .EnumerateArray() .Select(value => new { Kind = value.GetProperty("kind").GetString(), Path = value.GetProperty("path").GetString()! }) .ToArray(); Assert.Equal( manifestScenes.OrderBy(value => value, StringComparer.Ordinal), LegacyOperatorFolderValidator.RequiredSceneFileNames .OrderBy(value => value, StringComparer.Ordinal)); Assert.Equal( manifestAssets.Where(value => value.Kind != "Video") .Select(value => value.Path) .OrderBy(value => value, StringComparer.Ordinal), LegacyOperatorFolderValidator.RequiredBuiltInAssetRelativePaths .OrderBy(value => value, StringComparer.Ordinal)); Assert.Equal( manifestAssets.Where(value => value.Kind == "Video") .Select(value => value.Path) .OrderBy(value => value, StringComparer.Ordinal), LegacyOperatorFolderValidator.OptionalExternalAssetRelativePaths .OrderBy(value => value, StringComparer.Ordinal)); } [Fact] public void SceneValidation_MissingBaselineFailsWithoutLeakingThePath() { using var directory = new TemporaryDirectory(); var sceneDirectory = directory.CreateDirectory("sensitive-design-folder"); var result = LegacyOperatorFolderValidator.Validate( LegacyOperatorSettingsFolderKind.Scene, sceneDirectory); Assert.False(result.IsValid); Assert.Equal( LegacyOperatorFolderValidationFailure.RequiredFileMissing, result.Failure); Assert.Null(result.Folder); AssertPathFree(result.WarningMessage, sceneDirectory); } [Fact] public void BackgroundValidation_AcceptsAnEmptyFixedDriveDirectory() { using var directory = new TemporaryDirectory(); var backgroundDirectory = directory.CreateDirectory("Background"); Assert.True(LegacyOperatorFolderValidator.TryValidate( LegacyOperatorSettingsFolderKind.Background, backgroundDirectory, out var folder, out var warning)); Assert.Equal(Path.GetFullPath(backgroundDirectory), folder!.CanonicalPath); Assert.Null(warning); } [Fact] public void ResourceValidation_ParsesEveryClosedUiIniAndOnlyFlagsDatabaseIni() { using var directory = new TemporaryDirectory(); var resourceDirectory = directory.CreateDirectory("Res"); WriteValidResourceFiles(resourceDirectory); File.WriteAllText( Path.Combine(resourceDirectory, "MmoneyCoder.ini"), "secret-content-that-must-not-be-returned"); var stockMenu = LegacyRuntimeStockCutMenuLoader .ResolveFromResourceDirectory(resourceDirectory); Assert.False(stockMenu.UsesBuiltInCatalog); var uiCatalogs = LegacyRuntimeUiCatalogLoader .ResolveFromResourceDirectory(resourceDirectory); Assert.Empty(uiCatalogs.Warnings); var result = LegacyOperatorFolderValidator.Validate( LegacyOperatorSettingsFolderKind.Resource, resourceDirectory); Assert.True(result.IsValid, $"{result.Failure}: {result.WarningMessage}"); Assert.True(result.Folder!.DatabaseIniDetected); Assert.DoesNotContain("secret", result.Folder.AbbreviatedPath, StringComparison.OrdinalIgnoreCase); Assert.Null(result.WarningMessage); } [Fact] public void ResourceValidation_InvalidClosedIniFailsWithoutPartialAcceptance() { using var directory = new TemporaryDirectory(); var resourceDirectory = directory.CreateDirectory("private-runtime-resources"); WriteValidResourceFiles(resourceDirectory); WriteCp949( Path.Combine(resourceDirectory, "종목.ini"), ["[종목]", "등록되지_않은_컷"]); var result = LegacyOperatorFolderValidator.Validate( LegacyOperatorSettingsFolderKind.Resource, resourceDirectory); Assert.False(result.IsValid); Assert.Equal( LegacyOperatorFolderValidationFailure.InvalidRuntimeCatalog, result.Failure); Assert.Null(result.Folder); AssertPathFree(result.WarningMessage, resourceDirectory); } [Fact] public void ResourceValidation_RejectsAnUnsafeDatabaseIniWithoutReadingIt() { using var directory = new TemporaryDirectory(); var resourceDirectory = directory.CreateDirectory("Res"); WriteValidResourceFiles(resourceDirectory); Directory.CreateDirectory(Path.Combine(resourceDirectory, "MmoneyCoder.ini")); var result = LegacyOperatorFolderValidator.Validate( LegacyOperatorSettingsFolderKind.Resource, resourceDirectory); Assert.False(result.IsValid); Assert.Equal( LegacyOperatorFolderValidationFailure.RequiredFileUnsafe, result.Failure); Assert.Null(result.Folder); AssertPathFree(result.WarningMessage, resourceDirectory); } [Fact] public void Validation_RejectsRelativeMissingRootAndControlContainingPaths() { using var directory = new TemporaryDirectory(); var missing = Path.Combine(directory.Path, "missing-private-folder"); string?[] candidates = [ null, string.Empty, "relative-folder", missing, Path.GetPathRoot(directory.Path), directory.Path + "\nprivate" ]; foreach (var candidate in candidates) { var result = LegacyOperatorFolderValidator.Validate( LegacyOperatorSettingsFolderKind.Background, candidate); Assert.False(result.IsValid); Assert.Null(result.Folder); AssertPathFree(result.WarningMessage, missing); } } [Fact] public void Validation_RejectsAReparsePointInTheDirectoryAncestry_WhenSupported() { using var directory = new TemporaryDirectory(); var target = directory.CreateDirectory("target"); var link = Path.Combine(directory.Path, "link"); try { Directory.CreateSymbolicLink(link, target); } catch (Exception exception) when ( exception is IOException or UnauthorizedAccessException or PlatformNotSupportedException or NotSupportedException) { return; } try { var result = LegacyOperatorFolderValidator.Validate( LegacyOperatorSettingsFolderKind.Background, link); Assert.False(result.IsValid); Assert.Equal( LegacyOperatorFolderValidationFailure.ReparsePoint, result.Failure); } finally { Directory.Delete(link); } } [Fact] public void Load_MissingFileUsesDefaultsWithoutWarning() { using var directory = new TemporaryDirectory(); var store = new LegacyOperatorSettingsStore( Path.Combine(directory.Path, "Config", "runtime-folders.local.json")); var result = store.Load(); Assert.Equal(LegacyOperatorSettings.Default, result.Settings); Assert.False(result.LoadedFromDisk); Assert.Equal(LegacyOperatorSettingsStoreFailure.None, result.Failure); Assert.Null(result.WarningMessage); } [Fact] public void SaveAndLoad_RoundTripsOnlyTheVersionedClosedSchemaAtomically() { using var directory = new TemporaryDirectory(); var sceneDirectory = directory.CreateDirectory("Cuts"); WriteValidSceneFiles(sceneDirectory); var resourceDirectory = directory.CreateDirectory("Res"); WriteValidResourceFiles(resourceDirectory); var backgroundDirectory = directory.CreateDirectory("Background"); var configurationPath = Path.Combine( directory.Path, "Config", "runtime-folders.local.json"); var store = new LegacyOperatorSettingsStore(configurationPath); var settings = new LegacyOperatorSettings( sceneDirectory, resourceDirectory, backgroundDirectory, NavigationExpanded: false, ColorTheme: LegacyOperatorColorTheme.Dark, ViewMode: LegacyOperatorViewMode.Cards, StartWorkspace: LegacyOperatorStartWorkspace.LastWorkspace); var firstSave = store.Save(settings); var secondSave = store.Save(settings with { NavigationExpanded = true }); var loaded = store.Load(); Assert.True(firstSave.Succeeded); Assert.True(secondSave.Succeeded); Assert.NotNull(secondSave.SavedSettings); Assert.Null(secondSave.WarningMessage); Assert.True(loaded.LoadedFromDisk); Assert.True(loaded.Settings.NavigationExpanded); Assert.Equal(LegacyOperatorColorTheme.Dark, loaded.Settings.ColorTheme); Assert.Equal(LegacyOperatorViewMode.Cards, loaded.Settings.ViewMode); Assert.Equal( LegacyOperatorStartWorkspace.LastWorkspace, loaded.Settings.StartWorkspace); Assert.Equal(Path.GetFullPath(sceneDirectory), loaded.Settings.SceneDirectory); Assert.Equal(Path.GetFullPath(resourceDirectory), loaded.Settings.ResourceDirectory); Assert.Equal( Path.GetFullPath(backgroundDirectory), loaded.Settings.BackgroundDirectory); Assert.Empty(Directory.EnumerateFiles( Path.GetDirectoryName(configurationPath)!, "*.tmp")); using var document = JsonDocument.Parse(File.ReadAllBytes(configurationPath)); var names = document.RootElement.EnumerateObject() .Select(property => property.Name) .ToArray(); Assert.Equal( [ "schemaVersion", "sceneDirectory", "resourceDirectory", "backgroundDirectory", "navigationExpanded", "colorTheme", "viewMode", "startWorkspace" ], names); Assert.Equal( LegacyOperatorSettings.CurrentSchemaVersion, document.RootElement.GetProperty("schemaVersion").GetInt32()); Assert.Equal("dark", document.RootElement.GetProperty("colorTheme").GetString()); Assert.Equal("cards", document.RootElement.GetProperty("viewMode").GetString()); Assert.Equal( "lastWorkspace", document.RootElement.GetProperty("startWorkspace").GetString()); Assert.DoesNotContain( document.RootElement.EnumerateObject(), property => property.Name.Contains("password", StringComparison.OrdinalIgnoreCase) || property.Name.Contains("target", StringComparison.OrdinalIgnoreCase) || property.Name.Contains("live", StringComparison.OrdinalIgnoreCase)); } [Fact] public void Load_LegacySchemaUsesClosedAppearanceDefaults() { using var directory = new TemporaryDirectory(); var configurationPath = Path.Combine(directory.Path, "settings.json"); File.WriteAllText( configurationPath, "{\"schemaVersion\":1,\"sceneDirectory\":null,\"resourceDirectory\":null,\"backgroundDirectory\":null,\"navigationExpanded\":false}", new UTF8Encoding(false)); var result = new LegacyOperatorSettingsStore(configurationPath).Load(); Assert.True(result.LoadedFromDisk); Assert.Equal(LegacyOperatorSettingsStoreFailure.None, result.Failure); Assert.False(result.Settings.NavigationExpanded); Assert.Equal(LegacyOperatorColorTheme.System, result.Settings.ColorTheme); Assert.Equal(LegacyOperatorViewMode.Automatic, result.Settings.ViewMode); Assert.Equal( LegacyOperatorStartWorkspace.StockCut, result.Settings.StartWorkspace); } [Theory] [InlineData("{}", LegacyOperatorSettingsStoreFailure.InvalidJson)] [InlineData("{\"schemaVersion\":3,\"sceneDirectory\":null,\"resourceDirectory\":null,\"backgroundDirectory\":null,\"navigationExpanded\":true}", LegacyOperatorSettingsStoreFailure.UnsupportedSchema)] [InlineData("{\"schemaVersion\":1,\"sceneDirectory\":null,\"resourceDirectory\":null,\"backgroundDirectory\":null,\"navigationExpanded\":true,\"unexpected\":1}", LegacyOperatorSettingsStoreFailure.InvalidJson)] [InlineData("{\"schemaVersion\":1,\"schemaVersion\":1,\"sceneDirectory\":null,\"resourceDirectory\":null,\"backgroundDirectory\":null,\"navigationExpanded\":true}", LegacyOperatorSettingsStoreFailure.InvalidJson)] [InlineData("{\"SchemaVersion\":1,\"sceneDirectory\":null,\"resourceDirectory\":null,\"backgroundDirectory\":null,\"navigationExpanded\":true}", LegacyOperatorSettingsStoreFailure.InvalidJson)] [InlineData("{\"schemaVersion\":1,\"sceneDirectory\":\"relative\",\"resourceDirectory\":null,\"backgroundDirectory\":null,\"navigationExpanded\":true}", LegacyOperatorSettingsStoreFailure.InvalidSettings)] [InlineData("{\"schemaVersion\":2,\"sceneDirectory\":null,\"resourceDirectory\":null,\"backgroundDirectory\":null,\"navigationExpanded\":true,\"colorTheme\":\"mbn\",\"viewMode\":\"automatic\",\"startWorkspace\":\"stockCut\"}", LegacyOperatorSettingsStoreFailure.InvalidJson)] [InlineData("{\"schemaVersion\":2,\"sceneDirectory\":null,\"resourceDirectory\":null,\"backgroundDirectory\":null,\"navigationExpanded\":true,\"colorTheme\":\"system\",\"viewMode\":\"grid\",\"startWorkspace\":\"stockCut\"}", LegacyOperatorSettingsStoreFailure.InvalidJson)] [InlineData("{\"schemaVersion\":2,\"sceneDirectory\":null,\"resourceDirectory\":null,\"backgroundDirectory\":null,\"navigationExpanded\":true,\"colorTheme\":\"system\",\"viewMode\":\"automatic\",\"startWorkspace\":\"other\"}", LegacyOperatorSettingsStoreFailure.InvalidJson)] public void Load_StrictMalformedOrUnsafeJsonFailsClosed( string json, LegacyOperatorSettingsStoreFailure expectedFailure) { using var directory = new TemporaryDirectory(); var configurationPath = Path.Combine(directory.Path, "settings.json"); File.WriteAllText(configurationPath, json, new UTF8Encoding(false)); var store = new LegacyOperatorSettingsStore(configurationPath); var result = store.Load(); Assert.Equal(LegacyOperatorSettings.Default, result.Settings); Assert.False(result.LoadedFromDisk); Assert.Equal(expectedFailure, result.Failure); AssertPathFree(result.WarningMessage, directory.Path); } [Fact] public void Load_OversizedJsonFailsBeforeParsing() { using var directory = new TemporaryDirectory(); var configurationPath = Path.Combine(directory.Path, "settings.json"); using (var stream = new FileStream( configurationPath, FileMode.CreateNew, FileAccess.Write)) { stream.SetLength(LegacyOperatorSettingsStore.MaximumFileBytes + 1L); } var result = new LegacyOperatorSettingsStore(configurationPath).Load(); Assert.Equal(LegacyOperatorSettingsStoreFailure.FileTooLarge, result.Failure); Assert.Equal(LegacyOperatorSettings.Default, result.Settings); AssertPathFree(result.WarningMessage, configurationPath); } [Fact] public void Save_RejectsUnvalidatedPathsWithoutCreatingAConfigurationFile() { using var directory = new TemporaryDirectory(); var configurationPath = Path.Combine(directory.Path, "settings.json"); var store = new LegacyOperatorSettingsStore(configurationPath); var settings = LegacyOperatorSettings.Default with { SceneDirectory = Path.Combine(directory.Path, "not-present-private-cuts") }; var result = store.Save(settings); Assert.False(result.Succeeded); Assert.Equal(LegacyOperatorSettingsStoreFailure.InvalidSettings, result.Failure); Assert.Null(result.SavedSettings); Assert.False(File.Exists(configurationPath)); AssertPathFree(result.WarningMessage, settings.SceneDirectory!); } [Theory] [InlineData(99, 0, 0)] [InlineData(0, 99, 0)] [InlineData(0, 0, 99)] public void Save_RejectsUndefinedAppearanceValuesWithoutCreatingAConfigurationFile( int colorTheme, int viewMode, int startWorkspace) { using var directory = new TemporaryDirectory(); var configurationPath = Path.Combine(directory.Path, "settings.json"); var settings = LegacyOperatorSettings.Default with { ColorTheme = (LegacyOperatorColorTheme)colorTheme, ViewMode = (LegacyOperatorViewMode)viewMode, StartWorkspace = (LegacyOperatorStartWorkspace)startWorkspace }; var result = new LegacyOperatorSettingsStore(configurationPath).Save(settings); Assert.False(result.Succeeded); Assert.Equal(LegacyOperatorSettingsStoreFailure.InvalidSettings, result.Failure); Assert.False(File.Exists(configurationPath)); } [Fact] public void AbbreviateForDisplay_IsBoundedAndNeverReturnsInvalidInput() { using var directory = new TemporaryDirectory(); var longLeaf = new string('a', 80); var path = directory.CreateDirectory(longLeaf); var abbreviated = LegacyOperatorFolderValidator.AbbreviateForDisplay(path, 20); Assert.InRange(abbreviated.Length, 1, 20); Assert.Contains('…', abbreviated); Assert.Equal( LegacyOperatorFolderValidator.UnconfiguredDisplay, LegacyOperatorFolderValidator.AbbreviateForDisplay("relative\\path")); } private static void WriteValidResourceFiles(string resourceDirectory) { var stockLabel = LegacyStockCutCatalog.Rows .First(row => !row.IsSeparator) .RawLabel; WriteCp949( Path.Combine(resourceDirectory, "종목.ini"), ["[종목]", stockLabel]); WriteFixedFile( resourceDirectory, "해외.ini", LegacyFixedMarket.Overseas); WriteFixedFile( resourceDirectory, "환율.ini", LegacyFixedMarket.Exchange); WriteFixedFile( resourceDirectory, "지수.ini", LegacyFixedMarket.Index); WriteIndustryFile( resourceDirectory, "업종_코스피.ini", IndustryMarket.Kospi); WriteIndustryFile( resourceDirectory, "업종_코스닥.ini", IndustryMarket.Kosdaq); WriteComparisonFile(resourceDirectory); } private static void WriteValidSceneFiles(string sceneDirectory) { foreach (var fileName in LegacyOperatorFolderValidator.RequiredSceneFileNames) { File.WriteAllText(Path.Combine(sceneDirectory, fileName), "fixture-scene"); } foreach (var relativePath in LegacyOperatorFolderValidator.RequiredBuiltInAssetRelativePaths) { var path = Path.Combine(sceneDirectory, relativePath); Directory.CreateDirectory(Path.GetDirectoryName(path)!); File.WriteAllText(path, "fixture-asset"); } } private static void WriteFixedFile( string resourceDirectory, string fileName, LegacyFixedMarket market) { var lines = new List(); foreach (var section in LegacyFixedActionCatalog.Default .GetActions(market) .GroupBy(action => action.Section, StringComparer.Ordinal)) { lines.Add($"[{section.Key}]"); lines.AddRange(section.Select(action => action.Label)); } WriteCp949(Path.Combine(resourceDirectory, fileName), lines); } private static void WriteIndustryFile( string resourceDirectory, string fileName, IndustryMarket market) { var actions = LegacyIndustryActionCatalog.GetActions(market); var section = market == IndustryMarket.Kospi ? "코스피" : "코스닥"; WriteCp949( Path.Combine(resourceDirectory, fileName), [$"[{section}]", .. actions.Select(action => action.Label)]); } private static void WriteComparisonFile(string resourceDirectory) { var actions = LegacyComparisonWorkflowCatalog.Actions; var directActions = actions .Where(action => action.Kind != LegacyComparisonActionKind.Return) .ToArray(); var returnActions = actions .Where(action => action.Kind == LegacyComparisonActionKind.Return) .ToArray(); var lines = new List { "[지수]" }; lines.AddRange(LegacyComparisonWorkflowCatalog.FixedTargets .Select(target => target.DisplayName)); lines.AddRange(directActions.Select(action => $"[{action.Label}]")); lines.Add($"[{returnActions[0].Section}]"); lines.AddRange(returnActions.Select(action => action.Label)); WriteCp949(Path.Combine(resourceDirectory, "종목비교.ini"), lines); } private static void WriteCp949(string path, IEnumerable lines) => File.WriteAllText(path, string.Join("\r\n", lines) + "\r\n", Cp949); private static void AssertPathFree(string? warningMessage, string sensitivePath) { var warning = Assert.IsType(warningMessage); Assert.InRange(warning.Length, 1, 100); Assert.DoesNotContain(sensitivePath, warning, StringComparison.OrdinalIgnoreCase); Assert.DoesNotContain('\\', warning); Assert.DoesNotContain('/', warning); } private static Encoding CreateCp949Encoding() { Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); return Encoding.GetEncoding( 949, EncoderFallback.ExceptionFallback, DecoderFallback.ExceptionFallback); } private static string FindRepositoryRoot() { for (var current = new DirectoryInfo(AppContext.BaseDirectory); current is not null; current = current.Parent) { if (File.Exists(Path.Combine(current.FullName, "MBN_STOCK_WEBVIEW.sln"))) { return current.FullName; } } throw new DirectoryNotFoundException("Repository root was not found."); } private sealed class TemporaryDirectory : IDisposable { public TemporaryDirectory() { Path = System.IO.Path.Combine( System.IO.Path.GetTempPath(), $"MBN_STOCK_WEBVIEW_SETTINGS_{Guid.NewGuid():N}"); Directory.CreateDirectory(Path); } public string Path { get; } public string CreateDirectory(string name) { var path = System.IO.Path.Combine(Path, name); Directory.CreateDirectory(path); return path; } public void Dispose() { if (Directory.Exists(Path)) { Directory.Delete(Path, recursive: true); } } } }