Migrate remaining legacy operator workflows

This commit is contained in:
2026-07-12 05:39:27 +09:00
parent ffb8f43c19
commit a01836a2d7
132 changed files with 20566 additions and 720 deletions

View File

@@ -18,6 +18,7 @@ public sealed class PlayoutOptionsLoaderTests
"MBN_STOCK_PLAYOUT_LEGACY_BACKGROUND_ASSET",
"MBN_STOCK_PLAYOUT_LEGACY_BACKGROUND_VIDEO_LOOP_COUNT",
"MBN_STOCK_PLAYOUT_LEGACY_BACKGROUND_VIDEO_LOOP_INFINITE",
"MBN_STOCK_PLAYOUT_LEGACY_BACKGROUND_DIRECTORY",
"MBN_STOCK_PLAYOUT_SCENE_DIRECTORY",
"MBN_STOCK_PLAYOUT_TEST_WINDOW_TITLE_PATTERN",
"MBN_STOCK_PLAYOUT_QUEUE_CAPACITY",
@@ -53,6 +54,7 @@ public sealed class PlayoutOptionsLoaderTests
Assert.Null(options.LegacySceneBackgroundAssetPath);
Assert.Equal(2004, options.LegacySceneBackgroundVideoLoopCount);
Assert.True(options.LegacySceneBackgroundVideoLoopInfinite);
Assert.Null(options.LegacyBackgroundDirectory);
Assert.Empty(options.TestSceneAllowlist);
Assert.False(options.TrustedLiveOutputEnabled);
Assert.True(options.ReconnectEnabled);
@@ -74,6 +76,7 @@ public sealed class PlayoutOptionsLoaderTests
.Set("MBN_STOCK_PLAYOUT_LEGACY_BACKGROUND_ASSET", "Video\\studio.vrv")
.Set("MBN_STOCK_PLAYOUT_LEGACY_BACKGROUND_VIDEO_LOOP_COUNT", "2004")
.Set("MBN_STOCK_PLAYOUT_LEGACY_BACKGROUND_VIDEO_LOOP_INFINITE", "false")
.Set("MBN_STOCK_PLAYOUT_LEGACY_BACKGROUND_DIRECTORY", "C:\\env-backgrounds")
.Set("MBN_STOCK_PLAYOUT_SCENE_DIRECTORY", "C:\\env-scenes")
.Set("MBN_STOCK_PLAYOUT_TEST_WINDOW_TITLE_PATTERN", "ENV TEST")
.Set("MBN_STOCK_PLAYOUT_QUEUE_CAPACITY", "14")
@@ -125,6 +128,7 @@ public sealed class PlayoutOptionsLoaderTests
Assert.Equal("Video\\studio.vrv", options.LegacySceneBackgroundAssetPath);
Assert.Equal(2004, options.LegacySceneBackgroundVideoLoopCount);
Assert.False(options.LegacySceneBackgroundVideoLoopInfinite);
Assert.Equal("C:\\env-backgrounds", options.LegacyBackgroundDirectory);
Assert.Equal("C:\\env-scenes", options.SceneDirectory);
Assert.Equal("ENV TEST", options.TestProcessWindowTitlePattern);
Assert.Equal(14, options.QueueCapacity);

View File

@@ -368,6 +368,85 @@ public sealed class PlayoutSafetyValidationTests : IDisposable
ignoreCase: true);
}
[Fact]
public void ResolveCue_OperatorBackgroundUsesOnlyTheSeparateTrustedRoot()
{
using var backgrounds = TemporarySceneDirectory.Create("studio.vrv");
var options = ValidTestOptions();
options.LegacyBackgroundDirectory = backgrounds.Path;
var validated = ValidatedPlayoutOptions.Create(options);
var resolved = validated.ResolveCue(new PlayoutCue(
"test-scene.t2s",
"test-scene",
Mutations:
[
new PlayoutSetBackgroundVideo(
"studio.vrv",
LoopCount: 2004,
LoopInfinite: true,
AssetRoot: PlayoutAssetRoot.OperatorBackgroundDirectory)
]));
var video = Assert.IsType<PlayoutSetBackgroundVideo>(Assert.Single(resolved.Mutations!));
Assert.Equal(
Path.Combine(backgrounds.Path, "studio.vrv"),
video.AssetPath,
ignoreCase: true);
Assert.Equal(PlayoutAssetRoot.OperatorBackgroundDirectory, video.AssetRoot);
}
[Fact]
public void ResolveCue_OperatorBackgroundDoesNotFallBackToCutsRoot()
{
using var backgrounds = TemporarySceneDirectory.Create();
var options = ValidTestOptions();
options.LegacyBackgroundDirectory = backgrounds.Path;
var validated = ValidatedPlayoutOptions.Create(options);
Assert.Throws<PlayoutRequestException>(() => validated.ResolveCue(new PlayoutCue(
"test-scene.t2s",
"test-scene",
Mutations:
[
new PlayoutSetBackgroundVideo(
"Video\\background.vrv",
LoopCount: 2004,
LoopInfinite: true,
AssetRoot: PlayoutAssetRoot.OperatorBackgroundDirectory)
])));
}
[Fact]
public void ResolveCue_RevalidatesConfiguredBackgroundRootAtCommandTime()
{
var lateRoot = Path.Combine(Directory.GetParent(_scenes.Path)!.FullName, "late-background");
var options = ValidTestOptions();
options.LegacyBackgroundDirectory = lateRoot;
var validated = ValidatedPlayoutOptions.Create(options);
Directory.CreateDirectory(lateRoot);
File.WriteAllText(Path.Combine(lateRoot, "studio.vrv"), "late trusted background");
var resolved = validated.ResolveCue(new PlayoutCue(
"test-scene.t2s",
"test-scene",
Mutations:
[
new PlayoutSetBackgroundVideo(
"studio.vrv",
LoopCount: 2004,
LoopInfinite: true,
AssetRoot: PlayoutAssetRoot.OperatorBackgroundDirectory)
]));
var video = Assert.IsType<PlayoutSetBackgroundVideo>(Assert.Single(resolved.Mutations!));
Assert.Equal(
Path.Combine(lateRoot, "studio.vrv"),
video.AssetPath,
ignoreCase: true);
}
[Fact]
public void ResolveCue_AcceptsTheLegacy2004BackgroundVideoLoopValue()
{

View File

@@ -1,3 +1,4 @@
using MBN_STOCK_WEBVIEW.Core.Playout;
using MBN_STOCK_WEBVIEW.Core.Playout.Scenes;
namespace MBN_STOCK_WEBVIEW.Playout.Tests;
@@ -17,18 +18,24 @@ public sealed class PlayoutSceneCompositionFactoryTests
[Fact]
public void TrustedRelativeVideo_IsVerifiedEvenInDryRun()
{
using var scenes = TemporarySceneDirectory.Create("Video\\studio.vrv");
using var scenes = TemporarySceneDirectory.Create("5001.t2s");
var backgroundRoot = Path.Combine(Directory.GetParent(scenes.Path)!.FullName, "배경");
Directory.CreateDirectory(backgroundRoot);
File.WriteAllText(Path.Combine(backgroundRoot, "studio.vrv"), "trusted background");
var result = PlayoutSceneCompositionFactory.Create(new PlayoutOptions
{
Mode = PlayoutMode.DryRun,
SceneDirectory = scenes.Path,
LegacySceneBackgroundKind = LegacySceneBackgroundKind.Video,
LegacySceneBackgroundAssetPath = "Video\\studio.vrv"
LegacySceneBackgroundAssetPath = "studio.vrv"
});
Assert.Equal(LegacySceneBackgroundKind.Video, result.BackgroundKind);
Assert.Equal(Path.Combine("Video", "studio.vrv"), result.BackgroundAssetPath);
Assert.Equal("studio.vrv", result.BackgroundAssetPath);
Assert.Equal(2004, result.BackgroundVideoLoopCount);
Assert.Equal(
PlayoutAssetRoot.OperatorBackgroundDirectory,
result.BackgroundAssetRoot);
}
[Theory]
@@ -38,7 +45,13 @@ public sealed class PlayoutSceneCompositionFactoryTests
[InlineData("Video\\studio.exe")]
public void InvalidOrMissingBackground_FailsBeforeDryRunPrepare(string relativePath)
{
using var scenes = TemporarySceneDirectory.Create("Video\\studio.vrv");
using var scenes = TemporarySceneDirectory.Create("5001.t2s");
var backgroundRoot = Path.Combine(Directory.GetParent(scenes.Path)!.FullName, "배경");
Directory.CreateDirectory(backgroundRoot);
Directory.CreateDirectory(Path.Combine(backgroundRoot, "Video"));
File.WriteAllText(
Path.Combine(backgroundRoot, "Video", "studio.vrv"),
"trusted background");
var options = new PlayoutOptions
{
Mode = PlayoutMode.DryRun,
@@ -50,4 +63,33 @@ public sealed class PlayoutSceneCompositionFactoryTests
Assert.Throws<PlayoutConfigurationException>(() =>
PlayoutSceneCompositionFactory.Create(options));
}
[Fact]
public void LegacyDefault_UsesSiblingBackgroundRoot_NotTheCutsRoot()
{
using var scenes = TemporarySceneDirectory.Create("기본.vrv");
var backgroundRoot = Path.Combine(Directory.GetParent(scenes.Path)!.FullName, "배경");
Directory.CreateDirectory(backgroundRoot);
File.WriteAllText(
Path.Combine(backgroundRoot, PlayoutSceneCompositionFactory.LegacyDefaultBackgroundFileName),
"trusted background");
var result = PlayoutSceneCompositionFactory.CreateLegacyDefault(
new PlayoutOptions { SceneDirectory = scenes.Path },
fadeDuration: 6);
Assert.Equal("기본.vrv", result.BackgroundAssetPath);
Assert.Equal(PlayoutAssetRoot.OperatorBackgroundDirectory, result.BackgroundAssetRoot);
}
[Fact]
public void LegacyDefault_MissingSiblingDirectory_FailsClosed()
{
using var scenes = TemporarySceneDirectory.Create("기본.vrv");
Assert.Throws<PlayoutConfigurationException>(() =>
PlayoutSceneCompositionFactory.CreateLegacyDefault(
new PlayoutOptions { SceneDirectory = scenes.Path },
fadeDuration: 6));
}
}

View File

@@ -0,0 +1,122 @@
using System.Runtime.InteropServices;
namespace MBN_STOCK_WEBVIEW.Playout.Tests;
public sealed class TrustedPlayoutAssetPathTests
{
private static readonly IReadOnlySet<string> Allowed = new HashSet<string>(
[".vrv", ".png"],
StringComparer.OrdinalIgnoreCase);
[Fact]
public void ResolveRelativeFile_AcceptsOneInternalRegularFile()
{
using var root = TemporarySceneDirectory.Create("studio.vrv");
var resolved = TrustedPlayoutAssetPath.ResolveRelativeFile(
root.Path,
"studio.vrv",
Allowed);
Assert.Equal(Path.Combine(root.Path, "studio.vrv"), resolved, ignoreCase: true);
}
[Theory]
[InlineData("..\\outside.vrv")]
[InlineData("C:\\outside.vrv")]
public void ResolveRelativeFile_RejectsRootEscape(string input)
{
using var root = TemporarySceneDirectory.Create("studio.vrv");
Assert.Throws<TrustedPlayoutAssetException>(() =>
TrustedPlayoutAssetPath.ResolveRelativeFile(root.Path, input, Allowed));
}
[Fact]
public void ResolveRelativeFile_RejectsMissingFile()
{
using var root = TemporarySceneDirectory.Create();
Assert.Throws<TrustedPlayoutAssetException>(() =>
TrustedPlayoutAssetPath.ResolveRelativeFile(root.Path, "missing.vrv", Allowed));
}
[Fact]
public void ResolveAbsoluteFile_RejectsPickerSelectionOutsideTheRoot()
{
using var root = TemporarySceneDirectory.Create();
using var outside = TemporarySceneDirectory.Create("outside.vrv");
Assert.Throws<TrustedPlayoutAssetException>(() =>
TrustedPlayoutAssetPath.ResolveAbsoluteFile(
root.Path,
Path.Combine(outside.Path, "outside.vrv"),
Allowed));
}
[Fact]
public void ResolveRelativeFile_RejectsHardLinkedFile()
{
using var root = TemporarySceneDirectory.Create();
var parent = Directory.GetParent(root.Path)!.FullName;
var outside = Path.Combine(parent, "outside.vrv");
File.WriteAllText(outside, "outside trusted-looking bytes");
var link = Path.Combine(root.Path, "linked.vrv");
Assert.True(CreateHardLinkW(link, outside, IntPtr.Zero));
Assert.Throws<TrustedPlayoutAssetException>(() =>
TrustedPlayoutAssetPath.ResolveRelativeFile(root.Path, "linked.vrv", Allowed));
}
[Fact]
public void ResolveRelativeFile_RejectsReparseDirectory()
{
using var root = TemporarySceneDirectory.Create();
var parent = Directory.GetParent(root.Path)!.FullName;
var outside = Path.Combine(parent, "outside");
Directory.CreateDirectory(outside);
File.WriteAllText(Path.Combine(outside, "studio.vrv"), "outside bytes");
var link = Path.Combine(root.Path, "linked");
try
{
Directory.CreateSymbolicLink(link, outside);
}
catch (Exception exception) when (
exception is UnauthorizedAccessException or IOException or PlatformNotSupportedException)
{
throw Xunit.Sdk.SkipException.ForSkip(
"The Windows test host does not permit creating a symbolic link.");
}
Assert.Throws<TrustedPlayoutAssetException>(() =>
TrustedPlayoutAssetPath.ResolveRelativeFile(
root.Path,
"linked\\studio.vrv",
Allowed));
}
[Fact]
public void EnsureOpenedRegularFile_BindsTheHandleToTheExpectedPath()
{
using var root = TemporarySceneDirectory.Create("expected.vrv", "other.vrv");
var expected = Path.Combine(root.Path, "expected.vrv");
var other = Path.Combine(root.Path, "other.vrv");
using var stream = new FileStream(
other,
FileMode.Open,
FileAccess.Read,
FileShare.Read);
Assert.Throws<TrustedPlayoutAssetException>(() =>
TrustedPlayoutAssetPath.EnsureOpenedRegularFile(
stream.SafeFileHandle,
expected));
}
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool CreateHardLinkW(
string fileName,
string existingFileName,
IntPtr securityAttributes);
}