323 lines
13 KiB
C#
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);
|
|
}
|
|
}
|