Files
MBN_STOCK_WEBVIEW/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyRuntimeAssetDeploymentContractTests.cs

659 lines
26 KiB
C#

using System.Xml.Linq;
namespace MBN_STOCK_WEBVIEW.LegacyWeb.Tests;
public sealed class LegacyRuntimeAssetDeploymentContractTests
{
private static readonly string RepositoryRoot = FindRepositoryRoot();
private static readonly string ProjectPath = Path.Combine(
RepositoryRoot,
"src",
"MBN_STOCK_WEBVIEW.LegacyParityApp",
"MBN_STOCK_WEBVIEW.LegacyParityApp.csproj");
[Fact]
public void LegacyParityProjectLinksCutsAndResIntoOutputPublishAndMsix()
{
var project = XDocument.Load(ProjectPath);
var contents = project.Descendants("Content").ToArray();
AssertLinkedRuntimeTree(contents, "$(LegacyCutsSourceRoot)\\**\\*", "Cuts\\");
AssertLinkedRuntimeTree(contents, "@(LegacyPackagedResAsset)", "Res\\");
var packagedResNames = project.Descendants("LegacyPackagedResAsset")
.Select(element => Path.GetFileName(element.Attribute("Include")?.Value))
.ToHashSet(StringComparer.OrdinalIgnoreCase);
string[] requiredUiMenus =
[
"setup.ini",
"기타.ini",
"업종.ini",
"업종_코스닥.ini",
"업종_코스피.ini",
"종목.ini",
"종목_0111.ini",
"종목비교.ini",
"지수.ini",
"지수_0318.ini",
"지수_250401.ini",
"해외.ini",
"환율.ini"
];
Assert.Equal(34, packagedResNames.Count);
Assert.All(requiredUiMenus, name => Assert.Contains(name, packagedResNames));
Assert.DoesNotContain("MmoneyCoder.ini", packagedResNames);
Assert.DoesNotContain("MmoneyCoder - 복사본.ini", packagedResNames);
Assert.DoesNotContain("MmoneyCoder.ini.bak", packagedResNames);
Assert.DoesNotContain("MmoneyCoder.zip", packagedResNames);
Assert.DoesNotContain("afiedt.buf.txt", packagedResNames);
}
[Fact]
public void GraphEPreviewsExposeOnlyFourAllowlistedBitmapsThroughTheTrustedWebRoot()
{
var project = XDocument.Load(ProjectPath);
var previewNames = project.Descendants("LegacyFinancialPreviewAsset")
.Select(element => Path.GetFileName(element.Attribute("Include")?.Value)!)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
Assert.Equal(
new HashSet<string>(
["pie.bmp", "Grow.bmp", "sell.bmp", "profit.bmp"],
StringComparer.OrdinalIgnoreCase),
previewNames);
var previewContent = Assert.Single(
project.Descendants("Content"),
element => string.Equals(
element.Attribute("Include")?.Value,
"@(LegacyFinancialPreviewAsset)",
StringComparison.Ordinal));
Assert.Equal(
"Web\\Previews\\%(Filename)%(Extension)",
previewContent.Element("Link")?.Value);
Assert.Equal(
"Web\\Previews\\%(Filename)%(Extension)",
previewContent.Element("TargetPath")?.Value);
Assert.Equal(
"PreserveNewest",
previewContent.Element("CopyToOutputDirectory")?.Value);
Assert.Equal(
"PreserveNewest",
previewContent.Element("CopyToPublishDirectory")?.Value);
Assert.DoesNotContain(
project.Descendants("Content"),
element => string.Equals(
element.Attribute("Include")?.Value,
"$(LegacyResSourceRoot)\\**\\*",
StringComparison.OrdinalIgnoreCase));
var window = File.ReadAllText(Path.Combine(
RepositoryRoot,
"src",
"MBN_STOCK_WEBVIEW.LegacyParityApp",
"MainWindow.xaml.cs"));
Assert.Equal(
1,
window.Split(
"SetVirtualHostNameToFolderMapping(",
StringSplitOptions.None).Length - 1);
Assert.Contains("LegacyBridgeProtocol.TrustedHost", window, StringComparison.Ordinal);
Assert.Contains("webRoot", window, StringComparison.Ordinal);
Assert.DoesNotContain(
"SetVirtualHostNameToFolderMapping(\"res",
window,
StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void CustomerWordmarkIsProjectedFromTheAllowlistedLegacyAssetIntoTheTrustedWebRoot()
{
var project = XDocument.Load(ProjectPath);
var brandAsset = Assert.Single(project.Descendants("LegacyBrandAsset"));
Assert.Equal(
"$(LegacyResSourceRoot)\\logo.png",
brandAsset.Attribute("Include")?.Value);
var brandContent = Assert.Single(
project.Descendants("Content"),
element => string.Equals(
element.Attribute("Include")?.Value,
"@(LegacyBrandAsset)",
StringComparison.Ordinal));
Assert.Equal(
"Web\\Brand\\%(Filename)%(Extension)",
brandContent.Element("Link")?.Value);
Assert.Equal(
"Web\\Brand\\%(Filename)%(Extension)",
brandContent.Element("TargetPath")?.Value);
Assert.Equal(
"PreserveNewest",
brandContent.Element("CopyToOutputDirectory")?.Value);
Assert.Equal(
"PreserveNewest",
brandContent.Element("CopyToPublishDirectory")?.Value);
var packagedResNames = project.Descendants("LegacyPackagedResAsset")
.Select(element => Path.GetFileName(element.Attribute("Include")?.Value))
.ToHashSet(StringComparer.OrdinalIgnoreCase);
Assert.Contains("logo.png", packagedResNames);
}
[Fact]
public void WebViewIgnoresItsPersistentCacheBeforeExactTrustedNavigation()
{
var window = File.ReadAllText(Path.Combine(
RepositoryRoot,
"src",
"MBN_STOCK_WEBVIEW.LegacyParityApp",
"MainWindow.xaml.cs"));
var mapping = window.IndexOf(
"SetVirtualHostNameToFolderMapping(",
StringComparison.Ordinal);
var disableCache = window.IndexOf(
"\"Network.setCacheDisabled\"",
StringComparison.Ordinal);
var navigation = window.IndexOf(
"Browser.Source = new Uri(",
StringComparison.Ordinal);
Assert.True(mapping >= 0);
Assert.True(disableCache > mapping);
Assert.True(navigation > disableCache);
Assert.Contains(
"CoreWebView2HostResourceAccessKind.DenyCors",
window,
StringComparison.Ordinal);
Assert.Equal(
1,
window.Split(
"\"Network.setCacheDisabled\"",
StringSplitOptions.None).Length - 1);
Assert.Contains(
"\"{\\\"cacheDisabled\\\":true}\"",
window,
StringComparison.Ordinal);
Assert.Contains(
"$\"https://{LegacyBridgeProtocol.TrustedHost}/index.html\"",
window,
StringComparison.Ordinal);
Assert.DoesNotContain(
"$\"https://{LegacyBridgeProtocol.TrustedHost}/index.html?",
window,
StringComparison.Ordinal);
}
[Fact]
public void DatabaseCredentialsStayInUserLocalConfigurationAndNeverEnterBuildOutputs()
{
var project = XDocument.Load(ProjectPath);
Assert.DoesNotContain(
project.Descendants().Where(element =>
element.Name.LocalName is "Content" or "None"),
element => (element.Attribute("Include")?.Value ?? string.Empty)
.Contains("MmoneyCoder.ini", StringComparison.OrdinalIgnoreCase));
Assert.DoesNotContain(
project.Descendants("UpToDateCheckInput"),
element => (element.Attribute("Include")?.Value ?? string.Empty)
.Contains("MmoneyCoder.ini", StringComparison.OrdinalIgnoreCase));
Assert.DoesNotContain(
project.Descendants("UpToDateCheckBuilt"),
element => (element.Attribute("Include")?.Value ?? string.Empty)
.Contains("MmoneyCoder.ini", StringComparison.OrdinalIgnoreCase));
Assert.DoesNotContain(
project.Descendants("Target"),
element => string.Equals(
element.Attribute("Name")?.Value,
"CopyLegacyDatabaseIniToLocalOutput",
StringComparison.Ordinal));
Assert.DoesNotContain(
project.Descendants("Target"),
element => string.Equals(
element.Attribute("Name")?.Value,
"RemoveLegacyDatabaseIniFromLocalOutput",
StringComparison.Ordinal));
var scrubTarget = Assert.Single(
project.Descendants("Target"),
element => string.Equals(
element.Attribute("Name")?.Value,
"ScrubLegacyDatabaseIniFromBuildOutput",
StringComparison.Ordinal));
Assert.Equal("PrepareForBuild", scrubTarget.Attribute("BeforeTargets")?.Value);
var scrubDelete = Assert.Single(scrubTarget.Elements("Delete"));
Assert.Equal(
"$(TargetDir)Res\\MmoneyCoder.ini",
scrubDelete.Attribute("Files")?.Value);
var validationTarget = Assert.Single(
project.Descendants("Target"),
element => string.Equals(
element.Attribute("Name")?.Value,
"ValidateLegacyRuntimeAssets",
StringComparison.Ordinal));
Assert.DoesNotContain(
validationTarget.Elements("Error"),
element => (element.Attribute("Condition")?.Value ?? string.Empty)
.Contains("MmoneyCoder.ini", StringComparison.OrdinalIgnoreCase));
var rejectionTarget = Assert.Single(
project.Descendants("Target"),
element => string.Equals(
element.Attribute("Name")?.Value,
"RejectLegacyDatabaseIniFromPackagePayload",
StringComparison.Ordinal));
Assert.Equal(
"_ComputeAppxPackagePayload",
rejectionTarget.Attribute("BeforeTargets")?.Value);
var forbiddenPayload = Assert.Single(
rejectionTarget.Descendants("_ForbiddenLegacyDatabasePackagePayload"));
Assert.Contains(
"%(PackagingOutputs.Filename)%(PackagingOutputs.Extension)",
forbiddenPayload.Attribute("Condition")?.Value,
StringComparison.Ordinal);
Assert.Single(rejectionTarget.Elements("Error"));
}
[Fact]
public void FreshCloneUsesIsolatedSourceOnlyOutputWithoutWeakeningFullBuilds()
{
var project = XDocument.Load(ProjectPath);
var sourceRootWasSpecified = Assert.Single(
project.Descendants("LegacyRuntimeSourceRootWasSpecified"));
Assert.Equal(
"'$(LegacyRuntimeSourceRoot)' != ''",
sourceRootWasSpecified.Attribute("Condition")?.Value);
var modes = project.Descendants("LegacyRuntimeAssetsMode").ToArray();
Assert.Equal(2, modes.Length);
var requiredMode = Assert.Single(
modes,
mode => string.Equals(mode.Value, "Required", StringComparison.Ordinal));
var requiredCondition = requiredMode.Attribute("Condition")?.Value;
Assert.Contains(
"$(LegacyRuntimeSourceRootWasSpecified)",
requiredCondition,
StringComparison.Ordinal);
Assert.Contains(
"'$(Configuration)' == 'Release'",
requiredCondition,
StringComparison.Ordinal);
Assert.Contains(
"$(GenerateAppxPackageOnBuild)",
requiredCondition,
StringComparison.Ordinal);
Assert.Contains("$(PublishAppxPackage)", requiredCondition, StringComparison.Ordinal);
var autoMode = Assert.Single(
modes,
mode => string.Equals(mode.Value, "Auto", StringComparison.Ordinal));
Assert.Equal(
"'$(LegacyRuntimeAssetsMode)' == ''",
autoMode.Attribute("Condition")?.Value);
var assetsEnabled = Assert.Single(
project.Descendants("LegacyRuntimeAssetsEnabled"),
element => (element.Attribute("Condition")?.Value ?? string.Empty)
.Contains("Exists('$(LegacyRuntimeSourceRoot)')", StringComparison.Ordinal));
Assert.Equal("true", assetsEnabled.Value);
var sourceOnlyOutput = Assert.Single(project.Descendants("BaseOutputPath"));
Assert.Equal(@"bin\SourceOnly\", sourceOnlyOutput.Value);
Assert.Contains(
"$(LegacyRuntimeAssetsEnabled)",
sourceOnlyOutput.Attribute("Condition")?.Value,
StringComparison.Ordinal);
var sourceOnlyIntermediate = Assert.Single(
project.Descendants("IntermediateOutputPath"));
Assert.Equal(
@"obj\SourceOnly\$(Platform)\$(Configuration)\",
sourceOnlyIntermediate.Value);
Assert.Contains(
"$(LegacyRuntimeAssetsEnabled)",
sourceOnlyIntermediate.Attribute("Condition")?.Value,
StringComparison.Ordinal);
var sourceOnlyConstants = Assert.Single(project.Descendants("DefineConstants"));
Assert.Contains(
"SOURCE_ONLY_RUNTIME",
sourceOnlyConstants.Value,
StringComparison.Ordinal);
Assert.Contains(
"$(LegacyRuntimeAssetsEnabled)",
sourceOnlyConstants.Attribute("Condition")?.Value,
StringComparison.Ordinal);
var validationTarget = Assert.Single(
project.Descendants("Target"),
element => string.Equals(
element.Attribute("Name")?.Value,
"ValidateLegacyRuntimeAssets",
StringComparison.Ordinal));
var sourceOnlyMessage = Assert.Single(validationTarget.Elements("Message"));
Assert.Contains(
"$(LegacyRuntimeAssetsMode)",
sourceOnlyMessage.Attribute("Condition")?.Value,
StringComparison.Ordinal);
Assert.Contains(
"source-only DryRun",
sourceOnlyMessage.Attribute("Text")?.Value,
StringComparison.Ordinal);
var runtimeValidationErrors = validationTarget.Elements("Error")
.Where(error => (error.Attribute("Condition")?.Value ?? string.Empty)
.Contains("Exists(", StringComparison.Ordinal))
.ToArray();
Assert.NotEmpty(runtimeValidationErrors);
Assert.All(runtimeValidationErrors, error =>
Assert.Contains(
"$(LegacyRuntimeAssetsValidationEnabled)",
error.Attribute("Condition")?.Value,
StringComparison.Ordinal));
string[] externalContentIncludes =
[
@"$(LegacyCutsSourceRoot)\**\*",
"@(LegacyPackagedResAsset)",
"@(LegacyFinancialPreviewAsset)",
"@(LegacyBrandAsset)"
];
Assert.All(externalContentIncludes, include =>
{
var content = Assert.Single(
project.Descendants("Content"),
element => string.Equals(
element.Attribute("Include")?.Value,
include,
StringComparison.Ordinal));
Assert.Contains(
"$(LegacyRuntimeAssetsEnabled)",
content.Attribute("Condition")?.Value,
StringComparison.Ordinal);
});
Assert.DoesNotContain(
project.Descendants("Target"),
element => string.Equals(
element.Attribute("Name")?.Value,
"CopyLegacyDatabaseIniToLocalOutput",
StringComparison.Ordinal));
var mainWindow = File.ReadAllText(Path.Combine(
RepositoryRoot,
"src",
"MBN_STOCK_WEBVIEW.LegacyParityApp",
"MainWindow.xaml.cs"));
Assert.Contains("#if SOURCE_ONLY_RUNTIME", mainWindow, StringComparison.Ordinal);
Assert.Contains(
"Source-only builds do not initialize a database runtime.",
mainWindow,
StringComparison.Ordinal);
var playout = File.ReadAllText(Path.Combine(
RepositoryRoot,
"src",
"MBN_STOCK_WEBVIEW.LegacyParityApp",
"MainWindow.Playout.cs"));
Assert.Contains("forceSafeDryRun: IsSourceOnlyBuild", playout,
StringComparison.Ordinal);
}
[Fact]
public void LegacyRuntimeSourceRootIsOverrideableAndValidatedBeforeBuild()
{
var project = XDocument.Load(ProjectPath);
var sourceRoot = Assert.Single(
project.Descendants("LegacyRuntimeSourceRoot"),
element => element.Attribute("Condition") is not null);
Assert.Equal("'$(LegacyRuntimeSourceRoot)' == ''", sourceRoot.Attribute("Condition")!.Value);
Assert.Contains("MBN_STOCK_N\\MBN_STOCK_N\\bin\\Debug", sourceRoot.Value,
StringComparison.Ordinal);
var target = Assert.Single(
project.Descendants("Target"),
element => string.Equals(
element.Attribute("Name")?.Value,
"ValidateLegacyRuntimeAssets",
StringComparison.Ordinal));
Assert.Equal("PrepareForBuild", target.Attribute("BeforeTargets")?.Value);
var conditions = target.Elements("Error")
.Select(error => error.Attribute("Condition")?.Value)
.Where(condition => condition is not null)
.ToArray();
string[] requiredSentinels =
[
"$(LegacyRuntimeSourceRoot)",
"$(LegacyCutsSourceRoot)",
"$(LegacyResSourceRoot)",
"$(LegacyCutsSourceRoot)\\%(LegacyRequiredScene.Identity).t2s",
"$(LegacyCutsSourceRoot)\\%(LegacyRequiredBuiltInAsset.Identity)",
"$(LegacyResSourceRoot)\\logo.png",
"$(LegacyResSourceRoot)\\종목.ini",
"$(LegacyResSourceRoot)\\업종_코스피.ini",
"$(LegacyResSourceRoot)\\업종_코스닥.ini",
"$(LegacyResSourceRoot)\\해외.ini",
"$(LegacyResSourceRoot)\\환율.ini",
"$(LegacyResSourceRoot)\\지수.ini"
];
Assert.All(requiredSentinels, sentinel =>
Assert.Contains(conditions, condition =>
condition!.Contains(sentinel, StringComparison.Ordinal)));
var requiredScenes = Assert.Single(project.Descendants("LegacyRequiredScene"))
.Attribute("Include")!.Value
.Split(';', StringSplitOptions.RemoveEmptyEntries);
Assert.Equal(45, requiredScenes.Length);
Assert.Equal(requiredScenes.Length,
requiredScenes.Distinct(StringComparer.Ordinal).Count());
Assert.Contains("5001", requiredScenes);
Assert.Contains("5076", requiredScenes);
Assert.Contains("5079", requiredScenes);
Assert.Contains("6001", requiredScenes);
var requiredAssets = Assert.Single(
project.Descendants("LegacyRequiredBuiltInAsset"))
.Attribute("Include")!.Value
.Split(';', StringSplitOptions.RemoveEmptyEntries);
Assert.Equal(9, requiredAssets.Length);
Assert.Contains(@"images\주유기merge.png", requiredAssets);
Assert.Contains(@"images\35752913_l.jpg", requiredAssets);
Assert.Contains(@"Images\KRX.png", requiredAssets);
Assert.Contains(@"Images\NXT.png", requiredAssets);
Assert.DoesNotContain(
target.Elements("Error"),
error => (error.Attribute("Condition")?.Value ?? string.Empty)
.Contains("MmoneyCoder.ini", StringComparison.OrdinalIgnoreCase));
}
[Fact]
public void DevelopmentLiveHandoffUsesAnIgnoredVerifiedRuntimeRootAndSafeLocalFiles()
{
var directoryPropsPath = Path.Combine(RepositoryRoot, "Directory.Build.props");
var directoryProps = XDocument.Load(directoryPropsPath);
var localImport = Assert.Single(directoryProps.Descendants("Import"));
Assert.Equal(
"$(MSBuildThisFileDirectory)Directory.Build.local.props",
localImport.Attribute("Project")?.Value);
Assert.Contains(
"Exists(",
localImport.Attribute("Condition")?.Value,
StringComparison.Ordinal);
var gitIgnore = File.ReadAllLines(Path.Combine(RepositoryRoot, ".gitignore"));
Assert.Contains("Directory.Build.local.props", gitIgnore);
var initializer = File.ReadAllText(Path.Combine(
RepositoryRoot,
"scripts",
"Initialize-DevelopmentLiveConfig.ps1"));
Assert.Contains(
"[ValidateSet('127.0.0.1', '::1')]",
initializer,
StringComparison.Ordinal);
Assert.Contains("mode = 'DryRun'", initializer, StringComparison.Ordinal);
Assert.Contains(
"trustedLiveOutputEnabled = $true",
initializer,
StringComparison.Ordinal);
Assert.Contains(
"maximumReconnectAttempts = 0",
initializer,
StringComparison.Ordinal);
Assert.Contains("reconnectEnabled = $false", initializer, StringComparison.Ordinal);
Assert.Contains(
"maximumAutomaticRefreshesPerTakeIn = 0",
initializer,
StringComparison.Ordinal);
Assert.Contains(
"I_AUTHORIZE_LIVE_PROGRAM_OUTPUT_FOR_THIS_LAUNCH",
initializer,
StringComparison.Ordinal);
Assert.Contains(
"SetAccessRuleProtection($true, $false)",
initializer,
StringComparison.Ordinal);
var authorizationInvalidation = initializer.IndexOf(
"Invalidate-ExistingAuthorization -Path $authorizationPath",
StringComparison.Ordinal);
var baseConfigurationWrite = initializer.IndexOf(
"Write-ProtectedJson $playoutPath",
StringComparison.Ordinal);
Assert.True(authorizationInvalidation >= 0);
Assert.True(baseConfigurationWrite > authorizationInvalidation);
Assert.Contains(
"Write-ProtectedJson $authorizationPath",
initializer,
StringComparison.Ordinal);
}
[Fact]
public void DevelopmentRuntimeBundleRequiresIndependentHashAndClosedPayloadValidation()
{
var bundleCreator = File.ReadAllText(Path.Combine(
RepositoryRoot,
"scripts",
"New-LegacyRuntimeBundle.ps1"));
var bundleInitializer = File.ReadAllText(Path.Combine(
RepositoryRoot,
"scripts",
"Initialize-LegacyRuntimeBundle.ps1"));
Assert.Contains("$expectedResAssetCount = 34", bundleCreator, StringComparison.Ordinal);
Assert.Contains(
"Get-GitRepositoryAncestor -Path $sourceRoot",
bundleCreator,
StringComparison.Ordinal);
Assert.Contains(
"MmoneyCoder material is forbidden",
bundleCreator,
StringComparison.Ordinal);
Assert.Contains(
"LegacyRuntimeBundle.zip.sha256",
bundleCreator,
StringComparison.Ordinal);
Assert.Contains(
"[string] $ExpectedSha256",
bundleInitializer,
StringComparison.Ordinal);
Assert.Contains(
"ExpectedSha256 must be a separately delivered",
bundleInitializer,
StringComparison.Ordinal);
var archiveHashValidation = bundleInitializer.IndexOf(
"$actualArchiveHash.Equals(",
StringComparison.Ordinal);
var archiveOpen = bundleInitializer.IndexOf(
"New-Object IO.Compression.ZipArchive(",
StringComparison.Ordinal);
Assert.True(archiveHashValidation >= 0);
Assert.True(archiveOpen > archiveHashValidation);
Assert.Contains(
"Archive entry expanded beyond its declared length",
bundleInitializer,
StringComparison.Ordinal);
Assert.Contains(
"$actualCutsFileCount -le 0",
bundleInitializer,
StringComparison.Ordinal);
Assert.Contains(
"[Environment+SpecialFolder]::LocalApplicationData",
bundleInitializer,
StringComparison.Ordinal);
Assert.DoesNotContain(
"Get-NormalizedFullPath -Path $env:LOCALAPPDATA",
bundleInitializer,
StringComparison.Ordinal);
Assert.Contains(
"Existing Directory.Build.local.props is conditional, duplicated",
bundleInitializer,
StringComparison.Ordinal);
Assert.Contains(
"Assert-NoReparsePointInTree",
bundleInitializer,
StringComparison.Ordinal);
}
[Fact]
public void RepositoryDoesNotContainCopiedLegacyRuntimeTrees()
{
Assert.False(Directory.Exists(Path.Combine(RepositoryRoot, "Cuts")));
Assert.False(Directory.Exists(Path.Combine(RepositoryRoot, "Res")));
Assert.False(Directory.Exists(Path.Combine(
RepositoryRoot,
"src",
"MBN_STOCK_WEBVIEW.LegacyParityApp",
"Cuts")));
Assert.False(Directory.Exists(Path.Combine(
RepositoryRoot,
"src",
"MBN_STOCK_WEBVIEW.LegacyParityApp",
"Res")));
}
private static void AssertLinkedRuntimeTree(
IReadOnlyCollection<XElement> contents,
string include,
string targetPrefix)
{
var content = Assert.Single(
contents,
element => string.Equals(
element.Attribute("Include")?.Value,
include,
StringComparison.Ordinal));
Assert.StartsWith(targetPrefix, content.Element("Link")?.Value, StringComparison.Ordinal);
Assert.StartsWith(
targetPrefix,
content.Element("TargetPath")?.Value,
StringComparison.Ordinal);
Assert.Equal("PreserveNewest", content.Element("CopyToOutputDirectory")?.Value);
Assert.Equal("PreserveNewest", content.Element("CopyToPublishDirectory")?.Value);
}
private static string FindRepositoryRoot()
{
var current = new DirectoryInfo(AppContext.BaseDirectory);
while (current is not null)
{
if (File.Exists(Path.Combine(current.FullName, "MBN_STOCK_WEBVIEW.sln")))
{
return current.FullName;
}
current = current.Parent;
}
throw new DirectoryNotFoundException("Could not locate the repository root.");
}
}