using System.Text; using MBN_STOCK_WEBVIEW.Infrastructure; using MMoneyCoderSharp.Data; namespace MBN_STOCK_WEBVIEW.Infrastructure.Tests; public sealed class LegacyIniDatabaseOptionsLoaderTests { private const string IntegrationPathEnvironmentVariable = "MBN_STOCK_LEGACY_INI_INTEGRATION_PATH"; [Fact] public void Load_Utf8LegacyFile_MapsBothProvidersAndKeepsLastValue() { using var file = TemporaryLegacyIni.Create( Encoding.UTF8.GetBytes( """ [Oracle] //ConnectionName=ignored.invalid:1/IGNORED ConnectionName=oracle.dev.invalid:1522/DEVORA ID=old-user ID=oracle-user Pass=oracle=password [Maria] ConnectionName=maria.dev.invalid:3307/dev_stock ID=maria-user Pass=maria=password """)); var loader = new LegacyIniDatabaseOptionsLoader(_ => null); var options = loader.Load(file.Path); Assert.True(options.IsConfigured(DataSourceKind.Oracle)); Assert.True(options.IsConfigured(DataSourceKind.MariaDb)); Assert.Equal("oracle.dev.invalid", options.Oracle.Host); Assert.Equal(1522, options.Oracle.Port); Assert.Equal("DEVORA", options.Oracle.Sid); Assert.Null(options.Oracle.ServiceName); Assert.Equal("oracle-user", options.Oracle.UserName); Assert.Equal("oracle=password", options.Oracle.Password); Assert.Equal("maria.dev.invalid", options.MariaDb.Host); Assert.Equal(3307, options.MariaDb.Port); Assert.Equal("dev_stock", options.MariaDb.Database); Assert.Equal("maria-user", options.MariaDb.UserName); Assert.Equal("maria=password", options.MariaDb.Password); Assert.Equal(MariaDbTlsMode.Disabled, options.MariaDb.TlsMode); } [Fact] public void Load_Cp949LegacyFile_PreservesKoreanValues() { Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); var cp949 = Encoding.GetEncoding( 949, EncoderFallback.ExceptionFallback, DecoderFallback.ExceptionFallback); using var file = TemporaryLegacyIni.Create(cp949.GetBytes( """ [Oracle] ConnectionName=oracle.dev.invalid:1521/개발 ID=오라클사용자 Pass=암호 [Maria] ConnectionName=maria.dev.invalid:3306/주식 ID=마리아사용자 Pass=암호2 """)); var options = new LegacyIniDatabaseOptionsLoader(_ => null).Load(file.Path); Assert.Equal("개발", options.Oracle.Sid); Assert.Equal("오라클사용자", options.Oracle.UserName); Assert.Equal("주식", options.MariaDb.Database); Assert.Equal("마리아사용자", options.MariaDb.UserName); } [Fact] public void Load_EnvironmentStillOverridesLegacyValues() { using var file = TemporaryLegacyIni.CreateValidUtf8(); var environment = new Dictionary(StringComparer.Ordinal) { ["MBN_STOCK_ORACLE_HOST"] = "override-oracle.invalid", ["MBN_STOCK_MARIADB_TLS_MODE"] = "Required" }; var loader = new LegacyIniDatabaseOptionsLoader( name => environment.TryGetValue(name, out var value) ? value : null); var options = loader.Load(file.Path); Assert.Equal("override-oracle.invalid", options.Oracle.Host); Assert.Equal(MariaDbTlsMode.Required, options.MariaDb.TlsMode); } [Fact] public void Load_InvalidFile_DoesNotExposeFileValues() { const string secret = "sensitive-password-sentinel"; using var file = TemporaryLegacyIni.Create(Encoding.UTF8.GetBytes( $"[Oracle]\nConnectionName=invalid\nID=user\nPass={secret}")); var exception = Assert.Throws( () => new LegacyIniDatabaseOptionsLoader(_ => null).Load(file.Path)); Assert.DoesNotContain(secret, exception.ToString(), StringComparison.Ordinal); Assert.DoesNotContain(file.Path, exception.ToString(), StringComparison.Ordinal); } [Fact] public void Runtime_PrefersExecutableResIni_AndFallsBackWhenAbsent() { using var directory = TemporaryDirectory.Create(); var res = Path.Combine(directory.Path, "Res"); Directory.CreateDirectory(res); File.WriteAllBytes( Path.Combine(res, "MmoneyCoder.ini"), TemporaryLegacyIni.ValidUtf8Bytes()); var runtime = DatabaseRuntime.CreateLegacyExecutableDefault( directory.Path, Path.Combine(directory.Path, "missing.json")); Assert.Equal("oracle.dev.invalid", runtime.Options.Oracle.Host); File.Delete(Path.Combine(res, "MmoneyCoder.ini")); var fallback = DatabaseRuntime.CreateLegacyExecutableDefault( directory.Path, Path.Combine(directory.Path, "missing.json"), Path.Combine(directory.Path, "missing-legacy.ini")); Assert.False(fallback.Options.IsConfigured(DataSourceKind.Oracle)); Assert.False(fallback.Options.IsConfigured(DataSourceKind.MariaDb)); } [Fact] public void Runtime_UsesLocalLegacyOverrideWhenPackageDoesNotContainCredentials() { using var directory = TemporaryDirectory.Create(); using var overrideFile = TemporaryLegacyIni.CreateValidUtf8(); var runtime = DatabaseRuntime.CreateLegacyExecutableDefault( directory.Path, Path.Combine(directory.Path, "missing.json"), overrideFile.Path); Assert.Equal("oracle.dev.invalid", runtime.Options.Oracle.Host); Assert.Equal("maria.dev.invalid", runtime.Options.MariaDb.Host); } [Fact] public void Runtime_UserSelectedResCanExplicitlyPrecedeExecutableCredentials() { using var directory = TemporaryDirectory.Create(); var executableRes = Path.Combine(directory.Path, "Res"); Directory.CreateDirectory(executableRes); File.WriteAllBytes( Path.Combine(executableRes, "MmoneyCoder.ini"), TemporaryLegacyIni.ValidUtf8Bytes()); using var selected = TemporaryLegacyIni.Create(Encoding.UTF8.GetBytes( """ [Oracle] ConnectionName=selected.oracle.invalid:1521/DEVORA ID=oracle-user Pass=oracle-password [Maria] ConnectionName=selected.maria.invalid:3306/dev_stock ID=maria-user Pass=maria-password """)); var runtime = DatabaseRuntime.CreateLegacyExecutableDefault( directory.Path, Path.Combine(directory.Path, "missing.json"), selected.Path, preferLegacyOverride: true); Assert.Equal("selected.oracle.invalid", runtime.Options.Oracle.Host); Assert.Equal("selected.maria.invalid", runtime.Options.MariaDb.Host); } [Fact] public void Runtime_DevelopmentLiveUsesProtectedLocalAppDataIniWhenNoOperatorResIsSelected() { using var localApplicationData = TemporaryDirectory.Create(); var resourceDirectory = Path.Combine( localApplicationData.Path, "MBN_STOCK_WEBVIEW", "Res"); Directory.CreateDirectory(resourceDirectory); File.WriteAllBytes( Path.Combine(resourceDirectory, "MmoneyCoder.ini"), TemporaryLegacyIni.ValidUtf8Bytes()); const string environmentName = "MBN_STOCK_ORACLE_HOST"; var originalEnvironment = Environment.GetEnvironmentVariable(environmentName); DatabaseRuntime runtime; try { Environment.SetEnvironmentVariable( environmentName, "stale-environment.invalid"); runtime = DatabaseRuntime.CreateDevelopmentLiveLocalOverride( localApplicationData.Path, operatorResourceDirectory: null); } finally { Environment.SetEnvironmentVariable( environmentName, originalEnvironment); } Assert.Equal("oracle.dev.invalid", runtime.Options.Oracle.Host); Assert.Equal("maria.dev.invalid", runtime.Options.MariaDb.Host); } [Fact] public void Runtime_DevelopmentLiveSelectedResIniIsAuthoritativeAndIgnoresEnvironment() { using var localApplicationData = TemporaryDirectory.Create(); var protectedResourceDirectory = Path.Combine( localApplicationData.Path, "MBN_STOCK_WEBVIEW", "Res"); Directory.CreateDirectory(protectedResourceDirectory); File.WriteAllBytes( Path.Combine(protectedResourceDirectory, "MmoneyCoder.ini"), TemporaryLegacyIni.ValidUtf8Bytes()); using var selected = TemporaryLegacyIni.Create(Encoding.UTF8.GetBytes( """ [Oracle] ConnectionName=selected.oracle.invalid:1523/SELECTED ID=selected-oracle-user Pass=selected-oracle-password [Maria] ConnectionName=selected.maria.invalid:3308/selected_stock ID=selected-maria-user Pass=selected-maria-password """)); var environmentOverrides = new Dictionary { ["MBN_STOCK_ORACLE_HOST"] = "stale-environment.invalid", ["MBN_STOCK_ORACLE_PORT"] = "not-an-integer", ["MBN_STOCK_MARIADB_TLS_MODE"] = "Required" }; var originalEnvironment = environmentOverrides.Keys.ToDictionary( static name => name, Environment.GetEnvironmentVariable, StringComparer.Ordinal); DatabaseRuntime runtime; try { foreach (var pair in environmentOverrides) { Environment.SetEnvironmentVariable(pair.Key, pair.Value); } runtime = DatabaseRuntime.CreateDevelopmentLiveLocalOverride( localApplicationData.Path, selected.Directory); } finally { foreach (var pair in originalEnvironment) { Environment.SetEnvironmentVariable(pair.Key, pair.Value); } } Assert.Equal("selected.oracle.invalid", runtime.Options.Oracle.Host); Assert.Equal(1523, runtime.Options.Oracle.Port); Assert.Equal("SELECTED", runtime.Options.Oracle.Sid); Assert.Equal("selected.maria.invalid", runtime.Options.MariaDb.Host); Assert.Equal(3308, runtime.Options.MariaDb.Port); Assert.Equal("selected_stock", runtime.Options.MariaDb.Database); Assert.Equal(MariaDbTlsMode.Disabled, runtime.Options.MariaDb.TlsMode); } [Fact] public void Runtime_DevelopmentLiveReadsSelectedIniOnEveryCreation() { using var selected = TemporaryLegacyIni.Create(Encoding.UTF8.GetBytes( """ [Oracle] ConnectionName=first.oracle.invalid:1521/FIRST ID=oracle-user Pass=oracle-password [Maria] ConnectionName=first.maria.invalid:3306/first_stock ID=maria-user Pass=maria-password """)); var first = DatabaseRuntime.CreateDevelopmentLiveLocalOverride(selected.Directory); File.WriteAllBytes(selected.Path, Encoding.UTF8.GetBytes( """ [Oracle] ConnectionName=second.oracle.invalid:1521/SECOND ID=oracle-user Pass=oracle-password [Maria] ConnectionName=second.maria.invalid:3306/second_stock ID=maria-user Pass=maria-password """)); var second = DatabaseRuntime.CreateDevelopmentLiveLocalOverride(selected.Directory); Assert.Equal("first.oracle.invalid", first.Options.Oracle.Host); Assert.Equal("first.maria.invalid", first.Options.MariaDb.Host); Assert.Equal("second.oracle.invalid", second.Options.Oracle.Host); Assert.Equal("second.maria.invalid", second.Options.MariaDb.Host); } [Fact] public void Runtime_DevelopmentLiveSelectedResDoesNotDependOnLocalAppDataRoot() { using var selected = TemporaryLegacyIni.CreateValidUtf8(); var runtime = DatabaseRuntime.CreateDevelopmentLiveLocalOverride( localApplicationData: string.Empty, operatorResourceDirectory: selected.Directory); Assert.Equal("oracle.dev.invalid", runtime.Options.Oracle.Host); Assert.Equal("maria.dev.invalid", runtime.Options.MariaDb.Host); } [Fact] public void Runtime_DevelopmentLiveSelectedResDoesNotFallBackWhenExactIniIsMissing() { using var localApplicationData = TemporaryDirectory.Create(); var protectedResourceDirectory = Path.Combine( localApplicationData.Path, "MBN_STOCK_WEBVIEW", "Res"); Directory.CreateDirectory(protectedResourceDirectory); File.WriteAllBytes( Path.Combine(protectedResourceDirectory, "MmoneyCoder.ini"), TemporaryLegacyIni.ValidUtf8Bytes()); using var selectedResourceDirectory = TemporaryDirectory.Create(); File.WriteAllBytes( Path.Combine(selectedResourceDirectory.Path, "MmoneyCoder.ini.bak"), TemporaryLegacyIni.ValidUtf8Bytes()); Assert.Throws( () => DatabaseRuntime.CreateDevelopmentLiveLocalOverride( localApplicationData.Path, selectedResourceDirectory.Path)); } [Fact] public void Runtime_DevelopmentLiveSelectedResDoesNotFallBackWhenIniIsInvalid() { const string secret = "selected-secret-sentinel"; using var localApplicationData = TemporaryDirectory.Create(); var protectedResourceDirectory = Path.Combine( localApplicationData.Path, "MBN_STOCK_WEBVIEW", "Res"); Directory.CreateDirectory(protectedResourceDirectory); File.WriteAllBytes( Path.Combine(protectedResourceDirectory, "MmoneyCoder.ini"), TemporaryLegacyIni.ValidUtf8Bytes()); using var selected = TemporaryLegacyIni.Create(Encoding.UTF8.GetBytes( """ [Oracle] ConnectionName=invalid ID=oracle-user Pass=selected-secret-sentinel """)); var exception = Assert.Throws( () => DatabaseRuntime.CreateDevelopmentLiveLocalOverride( localApplicationData.Path, selected.Directory)); Assert.DoesNotContain(secret, exception.ToString(), StringComparison.Ordinal); Assert.DoesNotContain(selected.Path, exception.ToString(), StringComparison.Ordinal); } [Fact] public void Runtime_DevelopmentLiveFailsClosedWhenBothProtectedProfilesAreMissing() { using var localApplicationData = TemporaryDirectory.Create(); Directory.CreateDirectory(Path.Combine( localApplicationData.Path, "MBN_STOCK_WEBVIEW", "Config")); Assert.Throws( () => DatabaseRuntime.CreateDevelopmentLiveLocalOverride( localApplicationData.Path, operatorResourceDirectory: null)); } [Fact] public void Runtime_DevelopmentLiveFallsBackToProtectedLocalAppDataJson() { using var localApplicationData = TemporaryDirectory.Create(); var configurationDirectory = Path.Combine( localApplicationData.Path, "MBN_STOCK_WEBVIEW", "Config"); Directory.CreateDirectory(configurationDirectory); File.WriteAllText( Path.Combine(configurationDirectory, "database.local.json"), """ { "oracle": { "host": "json.oracle.invalid", "port": 1521, "sid": "DEVORA", "serviceName": null, "userName": "oracle-user", "password": "oracle-password" }, "mariaDb": { "host": "json.maria.invalid", "port": 3306, "database": "dev_stock", "userName": "maria-user", "password": "maria-password", "tlsMode": "Required" } } """); var runtime = DatabaseRuntime.CreateDevelopmentLiveLocalOverride( localApplicationData.Path, operatorResourceDirectory: null); Assert.Equal("json.oracle.invalid", runtime.Options.Oracle.Host); Assert.Equal("json.maria.invalid", runtime.Options.MariaDb.Host); Assert.Equal(MariaDbTlsMode.Required, runtime.Options.MariaDb.TlsMode); } [Fact] public void Runtime_DevelopmentLiveRejectsNonOrdinaryIniTarget() { using var localApplicationData = TemporaryDirectory.Create(); var iniDirectory = Path.Combine( localApplicationData.Path, "MBN_STOCK_WEBVIEW", "Res", "MmoneyCoder.ini"); Directory.CreateDirectory(iniDirectory); Assert.Throws( () => DatabaseRuntime.CreateDevelopmentLiveLocalOverride( localApplicationData.Path, operatorResourceDirectory: null)); } [Fact] public void Runtime_DevelopmentLiveRejectsNonOrdinaryJsonTarget() { using var localApplicationData = TemporaryDirectory.Create(); var jsonDirectory = Path.Combine( localApplicationData.Path, "MBN_STOCK_WEBVIEW", "Config", "database.local.json"); Directory.CreateDirectory(jsonDirectory); Assert.Throws( () => DatabaseRuntime.CreateDevelopmentLiveLocalOverride( localApplicationData.Path, operatorResourceDirectory: null)); } [Fact] public void Load_OptionalRealLegacyIni_IsConfiguredWithoutPrintingValues() { var path = Environment.GetEnvironmentVariable( IntegrationPathEnvironmentVariable); if (string.IsNullOrWhiteSpace(path)) { return; } var options = new LegacyIniDatabaseOptionsLoader().Load(path); Assert.True(options.IsConfigured(DataSourceKind.Oracle)); Assert.True(options.IsConfigured(DataSourceKind.MariaDb)); } private sealed class TemporaryLegacyIni : IDisposable { private TemporaryLegacyIni(string directory, string path) { Directory = directory; Path = path; } public string Directory { get; } public string Path { get; } public static TemporaryLegacyIni CreateValidUtf8() => Create(ValidUtf8Bytes()); public static byte[] ValidUtf8Bytes() => Encoding.UTF8.GetBytes( """ [Oracle] ConnectionName=oracle.dev.invalid:1521/DEVORA ID=oracle-user Pass=oracle-password [Maria] ConnectionName=maria.dev.invalid:3306/dev_stock ID=maria-user Pass=maria-password """); public static TemporaryLegacyIni Create(byte[] bytes) { var directory = System.IO.Path.Combine( System.IO.Path.GetTempPath(), Guid.NewGuid().ToString("N")); System.IO.Directory.CreateDirectory(directory); var path = System.IO.Path.Combine(directory, "MmoneyCoder.ini"); File.WriteAllBytes(path, bytes); return new TemporaryLegacyIni(directory, path); } public void Dispose() => System.IO.Directory.Delete(Directory, recursive: true); } private sealed class TemporaryDirectory : IDisposable { private TemporaryDirectory(string path) => Path = path; public string Path { get; } public static TemporaryDirectory Create() { var path = System.IO.Path.Combine( System.IO.Path.GetTempPath(), Guid.NewGuid().ToString("N")); System.IO.Directory.CreateDirectory(path); return new TemporaryDirectory(path); } public void Dispose() => System.IO.Directory.Delete(Path, recursive: true); } }