Files

323 lines
13 KiB
C#

using System.Collections;
using System.Text;
using System.Text.Json.Nodes;
using MBN_STOCK_WEBVIEW.Core.Playout.Scenes;
namespace MBN_STOCK_WEBVIEW.LegacyApplication.Tests;
public sealed class LegacyFixedActionCatalogTests
{
[Fact]
public void EmbeddedCatalog_HasApprovedIdentityCountsAndOrder()
{
var catalog = LegacyFixedActionCatalog.Default;
Assert.Equal(LegacyFixedActionCatalog.ExpectedDocumentSha256, catalog.DocumentSha256);
Assert.Equal(
LegacyFixedActionCatalog.ExpectedGeneratorSourceSha256,
catalog.GeneratorSourceSha256);
Assert.Equal(328, catalog.Actions.Count);
Assert.Equal(Enumerable.Range(1, 328).Select(index => $"fixed-{index:000}"),
catalog.Actions.Select(action => action.Id));
Assert.Equal(78, catalog.GetActions(LegacyFixedMarket.Overseas).Count);
Assert.Equal(46, catalog.GetActions(LegacyFixedMarket.Exchange).Count);
Assert.Equal(204, catalog.GetActions(LegacyFixedMarket.Index).Count);
Assert.Equal(322, catalog.Actions.Count(action => action.Available));
Assert.Equal(6, catalog.Actions.Count(action => !action.Available));
}
[Fact]
public void EmbeddedCatalog_ClosesEverySceneIdentityAgainstCoreCatalog()
{
foreach (var action in LegacyFixedActionCatalog.Default.Actions)
{
var definition = Assert.Single(
LegacySceneCatalog.Definitions,
candidate => candidate.BuilderKey == action.BuilderKey);
Assert.Single(action.Aliases);
Assert.Equal(action.Code, action.Aliases[0]);
Assert.Contains(action.Code, definition.CutAliases);
Assert.Contains(
LegacySceneCatalog.FindByCutAlias(action.Code),
candidate => candidate.BuilderKey == action.BuilderKey);
}
}
[Fact]
public void RuntimeAssets_AreApprovedAndImmutable()
{
var catalog = LegacyFixedActionCatalog.Default;
Assert.Equal(
"bin/Debug/Res/해외.ini",
catalog.GetRuntimeAsset(LegacyFixedMarket.Overseas).RelativePath);
Assert.Equal(
"DBDC4EAC28BED9FD0DEABED7FE880F21CB49E48503FD3CCB95C0A29128818B2A",
catalog.GetRuntimeAsset(LegacyFixedMarket.Overseas).Sha256);
Assert.Equal(
"bin/Debug/Res/환율.ini",
catalog.GetRuntimeAsset(LegacyFixedMarket.Exchange).RelativePath);
Assert.Equal(
"bin/Debug/Res/지수.ini",
catalog.GetRuntimeAsset(LegacyFixedMarket.Index).RelativePath);
var dictionary = Assert.IsAssignableFrom<IDictionary>(catalog.RuntimeAssets);
Assert.True(dictionary.IsReadOnly);
Assert.Throws<NotSupportedException>(() =>
dictionary.Add(
LegacyFixedMarket.Overseas,
new LegacyFixedRuntimeAsset(LegacyFixedMarket.Overseas, "bad", "bad")));
}
[Fact]
public void QueryApi_ReturnsImmutableMarketAndSectionViews()
{
var catalog = LegacyFixedActionCatalog.Default;
var overseas = catalog.GetActions(LegacyFixedMarket.Overseas);
var sections = catalog.GetSections(LegacyFixedMarket.Overseas);
var firstSection = sections[0];
Assert.Equal(LegacyFixedMarket.Overseas, firstSection.Market);
Assert.Equal("1열판기본", firstSection.Name);
Assert.Equal(43, firstSection.Actions.Count);
Assert.Same(catalog.Actions[0], catalog.GetAction("fixed-001"));
Assert.Same(
firstSection.Actions[0],
catalog.GetActions(LegacyFixedMarket.Overseas, "1열판기본")[0]);
Assert.Empty(catalog.GetActions(LegacyFixedMarket.Overseas, "없는 섹션"));
var mutableActions = Assert.IsAssignableFrom<IList<LegacyFixedAction>>(overseas);
Assert.True(mutableActions.IsReadOnly);
Assert.Throws<NotSupportedException>(() => mutableActions.RemoveAt(0));
Assert.Null(catalog.FindAction(null));
Assert.Null(catalog.FindAction("fixed-999"));
Assert.Throws<KeyNotFoundException>(() => catalog.GetAction("fixed-999"));
Assert.Throws<ArgumentOutOfRangeException>(() =>
catalog.GetActions((LegacyFixedMarket)99));
}
[Fact]
public void Materialize_ComputesDateFromTrustedHostClock()
{
var now = new DateTimeOffset(2026, 7, 15, 23, 58, 0, TimeSpan.FromHours(9));
var result = LegacyFixedActionCatalog.Default.Materialize("fixed-248", now);
Assert.Equal("2026-07-15", result.Selection.DataCode);
Assert.Equal("", result.Selection.GraphicType);
Assert.Equal("$TODAY", result.Action.Selection.DataCode);
Assert.Equal(LegacyFixedMarket.Index, result.RuntimeAsset.Market);
}
[Theory]
[InlineData(0, "PRE_MARKET")]
[InlineData(12, "PRE_MARKET")]
[InlineData(13, "AFTER_MARKET")]
[InlineData(23, "AFTER_MARKET")]
public void Materialize_ComputesNxtSessionFromTrustedHostClock(int hour, string expected)
{
var now = new DateTimeOffset(2026, 7, 15, hour, 59, 0, TimeSpan.FromHours(9));
var result = LegacyFixedActionCatalog.Default.Materialize("fixed-263", now);
Assert.Equal(expected, result.Selection.GraphicType);
Assert.Equal("$NXT_SESSION", result.Action.Selection.GraphicType);
Assert.Equal("", result.Selection.DataCode);
}
[Fact]
public void Materialize_LeavesMovingAverageStateOutOfTheActionIdentity()
{
var result = LegacyFixedActionCatalog.Default.Materialize(
"fixed-136",
new DateTimeOffset(2026, 7, 15, 9, 0, 0, TimeSpan.FromHours(9)));
Assert.Equal(string.Empty, result.Selection.GraphicType);
Assert.Equal("", result.Action.Selection.GraphicType);
}
[Fact]
public void Materialize_RetainsGraphicTypeForOtherBuilders()
{
var result = LegacyFixedActionCatalog.Default.Materialize(
"fixed-001",
DateTimeOffset.Now);
Assert.Equal("1열판기본", result.Selection.GraphicType);
}
[Fact]
public void Materialize_ResolvesEveryTemplateWithoutAcceptingSelectionOverrides()
{
var catalog = LegacyFixedActionCatalog.Default;
var beforeBoundary = new DateTimeOffset(
2026, 7, 15, 12, 59, 0, TimeSpan.FromHours(9));
var afterBoundary = beforeBoundary.AddHours(1);
foreach (var action in catalog.Actions.Where(action => action.Available))
{
var before = catalog.Materialize(
action.Id,
beforeBoundary);
Assert.DoesNotContain('$', before.Selection.GroupCode);
Assert.DoesNotContain('$', before.Selection.Subject);
Assert.DoesNotContain('$', before.Selection.GraphicType);
Assert.DoesNotContain('$', before.Selection.Subtype);
Assert.DoesNotContain('$', before.Selection.DataCode);
if (action.DynamicDate)
{
Assert.Equal("2026-07-15", before.Selection.DataCode);
}
else if (action.DynamicNxtSession)
{
Assert.Equal("PRE_MARKET", before.Selection.GraphicType);
Assert.Equal(
"AFTER_MARKET",
catalog.Materialize(action.Id, afterBoundary).Selection.GraphicType);
}
else if (action.SupportsMovingAverages)
{
Assert.Equal(action.Selection.GraphicType, before.Selection.GraphicType);
}
else
{
Assert.Equal(action.Selection, before.Selection);
}
}
Assert.Equal(50, catalog.Actions.Count(action => action.DynamicDate));
Assert.Equal(28, catalog.Actions.Count(action => action.DynamicNxtSession));
Assert.Equal(74, catalog.Actions.Count(action => action.SupportsMovingAverages));
}
[Fact]
public void Materialize_RejectsUnknownAndUnavailableActions()
{
var catalog = LegacyFixedActionCatalog.Default;
Assert.Throws<KeyNotFoundException>(() =>
catalog.Materialize("fixed-999", DateTimeOffset.Now));
var exception = Assert.Throws<InvalidOperationException>(() =>
catalog.Materialize("fixed-245", DateTimeOffset.Now));
Assert.Contains("수동 순매도", exception.Message, StringComparison.Ordinal);
}
[Fact]
public void Loader_RejectsUnknownJsonMembersAndMissingRequiredMembers()
{
var unknown = ReadDocument();
unknown["unexpected"] = true;
AssertInvalid(unknown, "JSON schema");
var nestedUnknown = ReadDocument();
Action(nestedUnknown, 0)["selection"]!["webOverride"] = "5006";
AssertInvalid(nestedUnknown, "JSON schema");
var missing = ReadDocument();
missing.AsObject().Remove("schemaVersion");
AssertInvalid(missing, "JSON schema");
var nullActions = ReadDocument();
nullActions["actions"] = null;
AssertInvalid(nullActions, "actions is required");
}
[Fact]
public void Loader_RejectsDuplicateJsonMembers()
{
var original = Encoding.UTF8.GetString(ReadEmbeddedBytes());
var duplicate = original.Replace(
"\"schemaVersion\": 1,",
"\"schemaVersion\": 1,\n \"schemaVersion\": 1,",
StringComparison.Ordinal);
var exception = Assert.Throws<InvalidDataException>(() =>
LegacyFixedActionCatalog.LoadForValidation(Encoding.UTF8.GetBytes(duplicate)));
Assert.Contains("JSON schema", exception.Message, StringComparison.Ordinal);
Assert.IsType<System.Text.Json.JsonException>(exception.InnerException);
}
[Fact]
public void Loader_RejectsTamperedDocumentHash()
{
var tampered = ReadEmbeddedBytes().Concat([(byte)' ']).ToArray();
var exception = Assert.Throws<InvalidDataException>(() =>
LegacyFixedActionCatalog.LoadForValidation(
tampered,
LegacyFixedActionCatalog.ExpectedDocumentSha256));
Assert.Contains("hash is invalid", exception.Message, StringComparison.Ordinal);
}
[Fact]
public void Loader_RejectsUnknownBuilderCodeAndAlias()
{
var unknownBuilder = ReadDocument();
Action(unknownBuilder, 0)["builderKey"] = "s9999";
AssertInvalid(unknownBuilder, "unknown builder");
var wrongCode = ReadDocument();
Action(wrongCode, 0)["code"] = "5006";
AssertInvalid(wrongCode, "is not an alias");
var wrongAlias = ReadDocument();
Action(wrongAlias, 0)["aliases"] = new JsonArray("N5001");
AssertInvalid(wrongAlias, "sole alias");
}
[Fact]
public void Loader_RejectsNonSequentialIdAndSelectionPlaceholders()
{
var wrongId = ReadDocument();
Action(wrongId, 0)["id"] = "fixed-002";
AssertInvalid(wrongId, "action id at index 0");
var untrustedSelection = ReadDocument();
Action(untrustedSelection, 0)["selection"]!["graphicType"] = "$WEB_VALUE";
AssertInvalid(untrustedSelection, "unapproved selection placeholder");
var badDynamicDate = ReadDocument();
Action(badDynamicDate, 247)["dynamicDate"] = false;
AssertInvalid(badDynamicDate, "dynamic-date declaration");
}
[Fact]
public void Loader_RejectsTamperedRuntimeAssetMetadata()
{
var path = ReadDocument();
path["runtimeAssets"]!["overseas"]!["relativePath"] = "C:/untrusted/해외.ini";
AssertInvalid(path, "runtime asset metadata");
var hash = ReadDocument();
hash["runtimeAssets"]!["index"]!["sha256"] = new string('0', 64);
AssertInvalid(hash, "runtime asset metadata");
}
private static JsonNode ReadDocument() =>
JsonNode.Parse(ReadEmbeddedBytes()) ?? throw new InvalidOperationException();
private static byte[] ReadEmbeddedBytes()
{
using var stream = typeof(LegacyFixedActionCatalog).Assembly.GetManifestResourceStream(
"MBN_STOCK_WEBVIEW.LegacyApplication.Catalog.LegacyFixedActions.json")
?? throw new InvalidOperationException("Embedded test catalog was not found.");
using var buffer = new MemoryStream();
stream.CopyTo(buffer);
return buffer.ToArray();
}
private static JsonObject Action(JsonNode document, int index) =>
document["actions"]!.AsArray()[index]!.AsObject();
private static void AssertInvalid(JsonNode document, string expectedMessage)
{
var bytes = Encoding.UTF8.GetBytes(document.ToJsonString());
var exception = Assert.Throws<InvalidDataException>(() =>
LegacyFixedActionCatalog.LoadForValidation(bytes));
Assert.Contains(expectedMessage, exception.Message, StringComparison.OrdinalIgnoreCase);
}
}