fix: security, data-integrity, and doc-drift findings from repo audit

Consolidates duplicate KIS API client implementations (governance tests
were exercising an unused class instead of the one actually running in
production), closes a SQL injection path in the DB admin page, fixes a
migration that used MySQL-only syntax and had never actually applied
(confirmed against production), resyncs docs/db/quantengine.dbml with
all migrations, and removes a duplicate OMS·WMS·ERP frontend tree in
favor of src/frontend/. Also corrects several unverifiable/inflated
claims in the OMS planning docs and realigns CI/CD and architecture
documentation with what's actually in the repo.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 11:20:02 +09:00
parent 99943d9871
commit 70824c2afb
185 changed files with 2062 additions and 34521 deletions
@@ -1,6 +1,5 @@
using System.Reflection;
using QuantEngine.Infrastructure.External;
using QuantEngine.Infrastructure.Data;
using QuantEngine.Infrastructure.Services;
namespace QuantEngine.Core.Tests;
@@ -12,9 +11,7 @@ public class SecurityTests
[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));
var ex = Record.Exception(() => InvokeAssertReadOnly(path, trId));
Assert.Null(ex);
}
@@ -25,45 +22,34 @@ public class SecurityTests
[InlineData("/uapi/domestic-stock/v1/trading/order-cash", "FHKST01010100")]
public void AssertReadOnly_BlocksTradingPathsOrIds(string path, string trId)
{
var client = CreateClient();
var ex = Assert.Throws<TargetInvocationException>(() => InvokeAssertReadOnly(client, path, trId));
var ex = Assert.Throws<TargetInvocationException>(() => InvokeAssertReadOnly(path, trId));
Assert.IsType<InvalidOperationException>(ex.InnerException);
Assert.Contains("BLOCKED", ex.InnerException!.Message);
}
[Fact]
public void AssertReadOnly_BlocksKnownTradingTrIdPrefixes()
[Theory]
[InlineData("VTTC8434R00")]
[InlineData("TTTC9912U")]
[InlineData("VTTC5555X")]
public void AssertReadOnly_BlocksEntireTradingTrIdFamily_NotJustHardcodedCodes(string trId)
{
var client = CreateClient();
var ex = Assert.Throws<TargetInvocationException>(() => InvokeAssertReadOnly(client, "/uapi/domestic-stock/v1/quotations/inquire-price", "VTTC8434R00"));
// These TR_IDs are not among the previously hardcoded exact-match list — they only get
// blocked once the guard checks the true TTTC*/VTTC* prefix family instead of a fixed
// set of known order codes.
var ex = Assert.Throws<TargetInvocationException>(() =>
InvokeAssertReadOnly("/uapi/domestic-stock/v1/quotations/inquire-price", trId));
Assert.IsType<InvalidOperationException>(ex.InnerException);
Assert.Contains("TR_ID", ex.InnerException!.Message);
}
private static KisApiClient CreateClient()
private static void InvokeAssertReadOnly(string path, string trId)
{
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)
// QuantEngine.Infrastructure.Services.KisApiClient is the class actually DI-registered
// in Program.cs and running in production — a second, unused KisApiClient used to live
// under Infrastructure.External with its own independent AssertReadOnly implementation
// that this test suite exercised instead. That class has been removed.
var method = typeof(KisApiClient).GetMethod("AssertReadOnly", BindingFlags.Static | BindingFlags.NonPublic)
?? throw new InvalidOperationException("AssertReadOnly method not found.");
method.Invoke(client, new object[] { path, trId });
}
private sealed class DummyHandler : HttpMessageHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
=> Task.FromResult(new HttpResponseMessage(System.Net.HttpStatusCode.OK));
}
private sealed class NoopConnectionFactory : IDbConnectionFactory
{
public System.Data.IDbConnection CreateConnection() => throw new NotSupportedException("Not needed for read-only guard tests.");
method.Invoke(null, new object[] { path, trId });
}
}