using System.Reflection; using QuantEngine.Infrastructure.External; namespace QuantEngine.Core.Tests; public class SecurityTests { [Theory] [InlineData("/uapi/domestic-stock/v1/quotations/inquire-price", "FHKST01010100")] [InlineData("/uapi/domestic-stock/v1/quotations/inquire-investor", "FHKST01010900")] [InlineData("/uapi/domestic-stock/v1/quotations/inquire-daily-itemchartprice", "FHKST03010100")] public void AssertReadOnly_AllowsReadOnlyQuotationPaths(string path, string trId) { var client = CreateClient(); var ex = Record.Exception(() => InvokeAssertReadOnly(client, path, trId)); Assert.Null(ex); } [Theory] [InlineData("/uapi/domestic-stock/v1/trading/order-cash", "VTTC0802U")] [InlineData("/uapi/domestic-stock/v1/quotations/inquire-price", "TTTC084000")] [InlineData("/uapi/domestic-stock/v1/trading/order-cash", "FHKST01010100")] public void AssertReadOnly_BlocksTradingPathsOrIds(string path, string trId) { var client = CreateClient(); var ex = Assert.Throws(() => InvokeAssertReadOnly(client, path, trId)); Assert.IsType(ex.InnerException); Assert.Contains("BLOCKED", ex.InnerException!.Message); } [Fact] public void AssertReadOnly_BlocksKnownTradingTrIdPrefixes() { var client = CreateClient(); var ex = Assert.Throws(() => InvokeAssertReadOnly(client, "/uapi/domestic-stock/v1/quotations/inquire-price", "VTTC8434R00")); Assert.IsType(ex.InnerException); Assert.Contains("TR_ID", ex.InnerException!.Message); } private static KisApiClient CreateClient() { Environment.SetEnvironmentVariable("KIS_APP_Key_TEST", "mock-key"); Environment.SetEnvironmentVariable("KIS_APP_Secret_TEST", "mock-secret"); return new KisApiClient(new HttpClient(new DummyHandler()), new NoopConnectionFactory()); } private static void InvokeAssertReadOnly(KisApiClient client, string path, string trId) { var method = typeof(KisApiClient).GetMethod("AssertReadOnly", BindingFlags.Instance | BindingFlags.NonPublic) ?? throw new InvalidOperationException("AssertReadOnly method not found."); method.Invoke(client, new object[] { path, trId }); } private sealed class DummyHandler : HttpMessageHandler { protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) => Task.FromResult(new HttpResponseMessage(System.Net.HttpStatusCode.OK)); } private sealed class NoopConnectionFactory : QuantEngine.Infrastructure.Data.IDbConnectionFactory { public System.Data.IDbConnection CreateConnection() => throw new NotSupportedException("Not needed for read-only guard tests."); } }