security: hard-disable all KIS trading paths (AEG-X-016)
Blocks submit, status, cancel, and settlement before HTTP or database writes and removes the KIS polling recurring job. Evidence: concrete adapter test 1/1 passed with zero HTTP calls. WBS remains IN_PROGRESS pending endpoint/startup override evidence.
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
# AEG-X-016 — KIS order-submission hard-off
|
||||
|
||||
## Scope
|
||||
|
||||
- **WBS ID:** `AEG-X-016`; **Requirement / API / DB / Job / UI / Test:** `REQ-KIS-OFF` / `Cross` / `Cross` / `Cross` / `Cross` / `Cross`.
|
||||
- **Dependency / Gate:** `AEG-X-005` (completed) / `ALL`.
|
||||
- **Acceptance:** zero external KIS order calls; capability override audit and kill switch pass.
|
||||
|
||||
## Source / Assumption / Unknown / Decision Required
|
||||
|
||||
- **Source:** `AGENTS.md` hard prohibition on automatic orders/KIS submission; `CapabilityOptions` startup validation; `TradeEndpoints`, `TradeHandlers`, `KisTradeExecutionService`, `TradeStatusPollingJob`, and Program recurring-job registration.
|
||||
- **Assumption:** the user's instruction is an unconditional hard-off, not a feature flag. Buy, sell, poll, and settlement calls must remain blocked even if an environment value attempts to enable a capability.
|
||||
- **Unknown:** there is no approved release that authorizes re-enabling KIS. This change supplies no activation path.
|
||||
- **Decision Required:** a future enablement would require a separately approved release, complete capability/kill-switch audit, and revised WBS evidence; it is out of scope.
|
||||
|
||||
## Actual evidence and remaining acceptance
|
||||
|
||||
- `dotnet test tests/KArtSell.Integration.Tests/KArtSell.Integration.Tests.csproj -c Release --filter FullyQualifiedName~KisTradingHardOffTests --logger "trx;LogFileName=KisTradingHardOffTests_20260809.trx" --results-directory evidence/AEG-X-016`
|
||||
- Result: passed `1/1`; artifact: `evidence/AEG-X-016/KisTradingHardOffTests_20260809.trx`; SHA-256: `F41D1DF823F91705D322A629B08ADF0BBA03EA1BEFA01DEF47E00C9DAFF2470D`.
|
||||
- The test invokes submit, status, cancel, and settlement on the concrete KIS adapter and asserts that every call throws the hard-off exception before its HTTP handler is called (zero external calls).
|
||||
- The submit/poll/settlement handlers guard before any database write or adapter access, and Program removes the historic `trade-status-polling` recurring job.
|
||||
- **Not complete:** endpoint-level disabled response and a startup configuration-override audit remain to be executed before the full WBS acceptance is claimed.
|
||||
@@ -182,7 +182,7 @@ AEG-VS-24-04,S11,W23-24,VS-24,ReportAndNotify,ReportingNotifications,BE,REQ-RPT-
|
||||
AEG-VS-24-05,S11,W23-24,VS-24,ReportAndNotify,ReportingNotifications,ASYNC,REQ-RPT-001,RPT-01,MIG-RPT-001,J11R,UI-RPT-01,T-RPT-001,Event/Job/Inbox·재처리 구현,"J11R; integration event, JobRun, watermark, inbox","동일 ScopeKey 재실행 결과가 동일하고 중복 side effect 0, transient/business hold/poison이 분리됨",BE/SRE,Data/Ops,5,AEG-VS-24-04,G4-A,SOURCE+DESIGN_PROPOSAL,P1,PLANNED
|
||||
AEG-VS-24-06,S11,W23-24,VS-24,ReportAndNotify,ReportingNotifications,FE,REQ-RPT-001,RPT-01,MIG-RPT-001,J11R,UI-RPT-01,T-RPT-001,Vue feature·Zod·Query·컴포넌트 구현,UI-RPT-01; route/api/schema/queries/pages/components,loading/empty/partial/stale/warn/error/401/403/409/expired/readonly와 접근성·권한 경계가 검증됨,FE Lead,UX/QA,6,AEG-VS-24-05,G4-A,SOURCE+DESIGN_PROPOSAL,P1,PLANNED
|
||||
AEG-VS-24-07,S11,W23-24,VS-24,ReportAndNotify,ReportingNotifications,TESTOPS,REQ-RPT-001,RPT-01,MIG-RPT-001,J11R,UI-RPT-01,T-RPT-001,회귀·관제·Runbook·Rollback 증거,T-RPT-001; xUnit/Vitest/Playwright/Data/Replay; OTel/alert/runbook,Golden·integration·failure·replay·E2E와 metric/alert/Owner/Secondary/rollback rehearsal가 Release Evidence에 연결됨,QA/SRE,Module Owner,7,AEG-VS-24-06,G4-A,SOURCE+DESIGN_PROPOSAL,P0,PLANNED
|
||||
AEG-X-016,S12,W25-26,Cross,Cross-cutting,Broker,CAPABILITY,REQ-KIS-OFF,Cross,Cross,Cross,Cross,Cross,KIS 주문 제출 startup/CI/runtime 차단 고도화,KIS 주문 제출 startup/CI/runtime 차단,"외부 주문 호출 0, capability override 감사·kill-switch 통과",Security/Ops,Architect/QA,7,AEG-X-005,ALL,SOURCE+DESIGN_PROPOSAL,P0,PLANNED
|
||||
AEG-X-016,S12,W25-26,Cross,Cross-cutting,Broker,CAPABILITY,REQ-KIS-OFF,Cross,Cross,Cross,Cross,Cross,KIS 주문 제출 startup/CI/runtime 차단 고도화,KIS 주문 제출 startup/CI/runtime 차단,"외부 주문 호출 0, capability override 감사·kill-switch 통과",Security/Ops,Architect/QA,7,AEG-X-005,ALL,SOURCE+DESIGN_PROPOSAL,P0,IN_PROGRESS
|
||||
AEG-VS-23-01,S12,W25-26,VS-23,ReconcilePositionsAndFills,ExecutionReconciliation,GOV,REQ-RECON-001,"EXE-01/02,REC-01/02",MIG-EXE-001/002,J12,UI-RECON-01,T-RECON-001,정책·범위·실패상태 계약 확정,"REQ-RECON-001, VS-23 SLICE_SPEC, ADR/Decision Log",사용자 결과 '브로커·내부 주문·체결·보유·현금 break와 승인 정정'·비목표·권한·예외·Source/Assumption/Unknown이 승인됨,PM/Architect,Compliance/Owner,3,"VS-08,VS-16",Pilot-A,SOURCE+DESIGN_PROPOSAL,P0,PLANNED
|
||||
AEG-VS-23-02,S12,W25-26,VS-23,ReconcilePositionsAndFills,ExecutionReconciliation,DATA,REQ-RECON-001,"EXE-01/02,REC-01/02",MIG-EXE-001/002,J12,UI-RECON-01,T-RECON-001,데이터 시점·스키마·정합성 계약,"MIG-EXE-001/002, DATA_CONTRACT, DQ/lineage 규칙",published_at/revision/valid-time/hash/단위/격리/재처리와 소유자가 정의되고 overwrite 경로가 없음,Data Architect/DBA,Quant/QA,5,AEG-VS-23-01,Pilot-A,SOURCE+DESIGN_PROPOSAL,P0,PLANNED
|
||||
AEG-VS-23-03,S12,W25-26,VS-23,ReconcilePositionsAndFills,ExecutionReconciliation,DOMAIN,REQ-RECON-001,"EXE-01/02,REC-01/02",MIG-EXE-001/002,J12,UI-RECON-01,T-RECON-001,도메인 불변조건·상태전이 구현,"src/Modules/ExecutionReconciliation/ReconcilePositionsAndFills/Domain, 정책 결정표",순수 정책 테스트에서 우선순위·경계값·단조성·금지 전이가 통과하고 Infrastructure 의존이 없음,BE/Quant Lead,Architect/QA,6,AEG-VS-23-02,Pilot-A,SOURCE+DESIGN_PROPOSAL,P0,PLANNED
|
||||
|
||||
|
@@ -6,6 +6,7 @@ AEG-X-004,S0,Cross,DbUp 복구 rehearsal 고도화,COMPLETED,2026-08-06,"docs/CU
|
||||
AEG-X-005,S0,Cross,Security auth 고도화,COMPLETED,2026-08-04,"docs/decisions/ADR-SEC-001.md + tests/KArtSell.Integration.Tests/SecurityAuthenticationTests.cs (6 tests)",Security/BE,"✅ ADR-SEC-001 produced (OIDC/JWT/DevelopmentHeader tiers), SecurityAuthenticationTests.cs (6 tests): endpoint authorization, DevelopmentHeader mode check, secret logging prevention, secret hardcoding check, AI prompt PII, auth config validation. Acceptance_Evidence verified: '비개발 무인증 접근 0, secret/log/prompt 노출 0'"
|
||||
AEG-X-006,S0,Cross,Outbox publisher 고도화,COMPLETED,2026-08-04,"docs/CURRENT/ARTIFACTS/AEG-X-006_ACCEPTANCE_EVIDENCE.md + src/KArtSell.BuildingBlocks/Reliability/DapperOutboxWriter.cs + OutboxPollerJob.cs",BE/SRE,"✅ Outbox→Inbox async pipeline verified: DapperOutboxWriter (transactional), OutboxPollerJob (idempotent), DapperInboxStore (deduplication), 5 consumer implementations. Acceptance_Evidence: All criteria met. 177/177 tests PASS."
|
||||
AEG-X-007,S0,Cross,Serilog/OTel correlation 고도화,COMPLETED,2026-08-06,"tests/KArtSell.ArchitectureTests/PiiRedactionTests.cs (6 tests) + commit e7913db",SRE/Security,"✅ PII redaction policy VERIFIED: SSN/Email/CreditCard/ApiKey redaction (6 tests). Commit e7913db adds pattern-based sanitization validation. All tests PASS (249/253)."
|
||||
AEG-X-016,S12,Cross,KIS 주문 제출 startup/CI/runtime 차단 고도화,IN_PROGRESS,TBD,"docs/CURRENT/AEG-X-016_KIS_HARD_OFF_SLICE_NOTE.md; src/KArtSell.Modules.ModelOperations/TradeExecution/KisTradeExecutionService.cs; src/KArtSell.Modules.ModelOperations/TradeExecution/TradeHandlers.cs; src/KArtSell.Host/Program.cs; tests/KArtSell.Integration.Tests/TradeExecution/KisTradingHardOffTests.cs; evidence/AEG-X-016/KisTradingHardOffTests_20260809.trx",Security/Ops,"User-directed hard-off: concrete KIS submit/status/cancel/settlement adapter throws before HTTP; test proves zero HTTP calls (1/1). Trade handlers guard before DB writes and Program removes trade-status-polling. Remains IN_PROGRESS until endpoint-level disabled response and startup capability-override/kill-switch evidence are run. No KIS activation path was introduced."
|
||||
AEG-V15-033,S8,VS-18,Schedule anchor 계산기,COMPLETED,2026-08-09,"docs/CURRENT/AEG-V15-033_SCHEDULE_ANCHOR_SLICE_NOTE.md; src/KArtSell.Modules.ModelOperations/Domain/ScheduleOccurrencePlanner.cs; tests/KArtSell.ModelOperations.UnitTests/ScheduleOccurrencePlannerTests.cs; evidence/AEG-V15-033/ScheduleOccurrencePlannerTests_20260809.trx",BE Lead,"Actual Release run: dotnet test tests/KArtSell.ModelOperations.UnitTests/KArtSell.ModelOperations.UnitTests.csproj -c Release --filter FullyQualifiedName~ScheduleOccurrencePlannerTests; 2/2 passed. Characterizes the approved scheduledFor anchor and missed-occurrence skip behavior. No schedule was enabled; market-calendar/timezone (DEC-079), dispatch enqueue/mark atomicity (DEC-083), leases, database integration, and later scheduler WBS evidence remain out of scope."
|
||||
AEG-V15-034,S8,VS-18,Catch-up policy 구현,COMPLETED,2026-08-09,"docs/CURRENT/AEG-V15-034_CATCH_UP_POLICY_SLICE_NOTE.md; src/KArtSell.Modules.ModelOperations/Domain/ScheduleOccurrencePlanner.cs; src/KArtSell.Modules.ModelOperations/Scheduling/ModelOperationsDispatcherJob.cs; src/KArtSell.Modules.ModelOperations/Infrastructure/DapperModelScheduleRepository.cs; tests/KArtSell.ModelOperations.UnitTests/ScheduleOccurrencePlannerTests.cs; evidence/AEG-V15-034/ScheduleOccurrencePlannerTests_20260809.trx",BE Lead,"Actual Release run: targeted ScheduleOccurrencePlannerTests 4/4 passed. LATEST_ONLY dispatches only the latest anchored occurrence; SKIP_MISSED advances the lease-held schedule without enqueueing stale work; ALL_WITH_LIMIT dispatches only the configured most-recent occurrences, each with an occurrence-specific UTC idempotency key. Schedules remain disabled. Database integration, lease CAS, and DEC-083 enqueue/mark atomicity are not claimed and remain owned by later WBS items."
|
||||
AEG-V15-035,S8,VS-18,Due operation 계약 확장,COMPLETED,2026-08-09,"docs/CURRENT/AEG-V15-035_DUE_OPERATION_CONTRACT_SLICE_NOTE.md; src/KArtSell.Modules.ModelOperations/Application/ModelOperationsContracts.cs; src/KArtSell.Modules.ModelOperations/Application/ModelOperationRequestService.cs; src/KArtSell.Modules.ModelOperations/Scheduling/ScheduledModelOperationJob.cs; src/KArtSell.Modules.ModelOperations/Infrastructure/DapperModelOperationRequestRepository.cs; tests/KArtSell.ModelOperations.UnitTests/ModelOperationRequestServiceTests.cs; evidence/AEG-V15-035/DueModelOperationContractTests_20260809.trx",BE Lead,"Actual Release run: 5/5 targeted unit tests passed. The scheduler occurrence, catch-up policy, and max catch-up flow from due schedule through the serialized job and validated application request; scheduled_for is inserted in the normalized request model and all three values are retained in the transactional outbox payload. Schedules remain disabled. No new migration or PostgreSQL integration evidence is claimed: MIG-0020 already provides scheduled_for; policy and limit provenance is immutable in the event payload, while schedule configuration remains the normalized source referenced by schedule_id/version."
|
||||
|
||||
|
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TestRun id="cccd60ce-1f35-445e-b217-45bdcd0ff494" name="kjh20@KIMJAEHYUN-NOTE 2026-08-09 02:29:26" runUser="KIMJAEHYUN-NOTE\kjh20" xmlns="http://microsoft.com/schemas/VisualStudio/TeamTest/2010">
|
||||
<Times creation="2026-08-09T02:29:26.6955415+09:00" queuing="2026-08-09T02:29:26.6955420+09:00" start="2026-08-09T02:29:25.2360804+09:00" finish="2026-08-09T02:29:26.7031426+09:00" />
|
||||
<TestSettings name="default" id="59b4ac6a-e53e-4388-973c-13a98934a17d">
|
||||
<Deployment runDeploymentRoot="kjh20_KIMJAEHYUN-NOTE_2026-08-09_02_29_26" />
|
||||
</TestSettings>
|
||||
<Results>
|
||||
<UnitTestResult executionId="eae0eff0-1881-4872-94a4-be93cfb4af12" testId="27c052af-b196-e13a-e07a-4a09b3b4c2de" testName="KArtSell.Integration.Tests.TradeExecution.KisTradingHardOffTests.Every_kis_trade_operation_fails_before_any_http_call" computerName="KIMJAEHYUN-NOTE" duration="00:00:00.0226996" startTime="2026-08-09T02:29:26.5575081+09:00" endTime="2026-08-09T02:29:26.5870108+09:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="eae0eff0-1881-4872-94a4-be93cfb4af12" />
|
||||
</Results>
|
||||
<TestDefinitions>
|
||||
<UnitTest name="KArtSell.Integration.Tests.TradeExecution.KisTradingHardOffTests.Every_kis_trade_operation_fails_before_any_http_call" storage="d:\jobroomz\kartsell.aegis\tests\kartsell.integration.tests\bin\release\net10.0\kartsell.integration.tests.dll" id="27c052af-b196-e13a-e07a-4a09b3b4c2de">
|
||||
<Execution id="eae0eff0-1881-4872-94a4-be93cfb4af12" />
|
||||
<TestMethod codeBase="D:\JobRoomz\KArtSell.Aegis\tests\KArtSell.Integration.Tests\bin\Release\net10.0\KArtSell.Integration.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner3/netcore/" className="KArtSell.Integration.Tests.TradeExecution.KisTradingHardOffTests" name="Every_kis_trade_operation_fails_before_any_http_call" />
|
||||
</UnitTest>
|
||||
</TestDefinitions>
|
||||
<TestEntries>
|
||||
<TestEntry testId="27c052af-b196-e13a-e07a-4a09b3b4c2de" executionId="eae0eff0-1881-4872-94a4-be93cfb4af12" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
|
||||
</TestEntries>
|
||||
<TestLists>
|
||||
<TestList name="목록에 없는 결과" id="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
|
||||
<TestList name="로드된 모든 결과" id="19431567-8539-422a-85d7-44ee4e166bda" />
|
||||
</TestLists>
|
||||
<ResultSummary outcome="Completed">
|
||||
<Counters total="1" executed="1" passed="1" failed="0" error="0" timeout="0" aborted="0" inconclusive="0" passedButRunAborted="0" notRunnable="0" notExecuted="0" disconnected="0" warning="0" completed="0" inProgress="0" pending="0" />
|
||||
<Output>
|
||||
<StdOut>[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v3.1.5+1b188a7b0a (64-bit .NET 10.0.10)
|
||||
[xUnit.net 00:00:00.69] Discovering: KArtSell.Integration.Tests
|
||||
[xUnit.net 00:00:00.77] Discovered: KArtSell.Integration.Tests
|
||||
[xUnit.net 00:00:00.80] Starting: KArtSell.Integration.Tests
|
||||
[xUnit.net 00:00:00.86] Finished: KArtSell.Integration.Tests
|
||||
</StdOut>
|
||||
</Output>
|
||||
</ResultSummary>
|
||||
</TestRun>
|
||||
@@ -398,8 +398,8 @@ try { RecurringJob.AddOrUpdate<OpenDartDailyBatchJob>("opendart-daily-batch", jo
|
||||
try { RecurringJob.AddOrUpdate<GenerateDailyRecommendationJob>("daily-recommendation", job => job.ExecuteAsync(CancellationToken.None), "0 9 * * *", new RecurringJobOptions { TimeZone = kstTimeZone }); logger.LogInformation("✅ daily-recommendation registered"); } catch (Exception ex) { logger.LogWarning(ex, "⚠️ daily-recommendation error"); }
|
||||
try { RecurringJob.AddOrUpdate<GenerateWeeklyRecommendationJob>("weekly-recommendation", job => job.ExecuteAsync(CancellationToken.None), "0 9 * * 6", new RecurringJobOptions { TimeZone = kstTimeZone }); logger.LogInformation("✅ weekly-recommendation registered"); } catch (Exception ex) { logger.LogWarning(ex, "⚠️ weekly-recommendation error"); }
|
||||
try { RecurringJob.AddOrUpdate<GenerateMonthlyRecommendationJob>("monthly-recommendation", job => job.ExecuteAsync(CancellationToken.None), "0 9 1 * *", new RecurringJobOptions { TimeZone = kstTimeZone }); logger.LogInformation("✅ monthly-recommendation registered"); } catch (Exception ex) { logger.LogWarning(ex, "⚠️ monthly-recommendation error"); }
|
||||
// DEBT-027: without this, PollTradeStatusHandler/ConfirmSettlementHandler were reachable via DI but never invoked by anything.
|
||||
try { RecurringJob.AddOrUpdate<KArtSell.Host.Jobs.TradeStatusPollingJob>("trade-status-polling", job => job.ExecuteAsync(CancellationToken.None), "*/2 * * * *", new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc }); logger.LogInformation("✅ trade-status-polling registered"); } catch (Exception ex) { logger.LogWarning(ex, "⚠️ trade-status-polling error"); }
|
||||
RecurringJob.RemoveIfExists("trade-status-polling");
|
||||
logger.LogWarning("KIS trade-status-polling removed: KIS trading is hard-disabled.");
|
||||
|
||||
logger.LogInformation("🎯 Hangfire background jobs initialization complete");
|
||||
|
||||
|
||||
@@ -25,6 +25,17 @@ public class KisTradeExecutionException : Exception
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class KisTradingDisabledException : InvalidOperationException
|
||||
{
|
||||
public KisTradingDisabledException()
|
||||
: base("KIS trading is hard-disabled. No buy, sell, cancel, status, or settlement call is permitted.") { }
|
||||
}
|
||||
|
||||
public static class KisTradingHardOff
|
||||
{
|
||||
public static void EnsureDisabled() => throw new KisTradingDisabledException();
|
||||
}
|
||||
|
||||
public interface IKisTradeExecutionService
|
||||
{
|
||||
Task<(string OrderId, JsonElement Response)> ExecuteTradeAsync(Guid tradeId, int quantity, decimal limitPrice, Guid correlationId, CancellationToken ct = default);
|
||||
@@ -56,6 +67,7 @@ public class KisTradeExecutionService : IKisTradeExecutionService
|
||||
Guid correlationId,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
KisTradingHardOff.EnsureDisabled();
|
||||
var requestBody = new
|
||||
{
|
||||
symbol = "US0100",
|
||||
@@ -120,6 +132,7 @@ public class KisTradeExecutionService : IKisTradeExecutionService
|
||||
Guid correlationId,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
KisTradingHardOff.EnsureDisabled();
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, $"{KisApiBase}/orders/{kisOrderId}");
|
||||
request.Headers.Add("X-Correlation-ID", correlationId.ToString());
|
||||
|
||||
@@ -159,6 +172,7 @@ public class KisTradeExecutionService : IKisTradeExecutionService
|
||||
Guid correlationId,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
KisTradingHardOff.EnsureDisabled();
|
||||
var requestBody = new { reason = reason };
|
||||
var content = new StringContent(
|
||||
JsonSerializer.Serialize(requestBody),
|
||||
@@ -195,6 +209,7 @@ public class KisTradeExecutionService : IKisTradeExecutionService
|
||||
Guid correlationId,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
KisTradingHardOff.EnsureDisabled();
|
||||
var requestBody = new { confirm = true };
|
||||
var content = new StringContent(
|
||||
JsonSerializer.Serialize(requestBody),
|
||||
|
||||
@@ -42,6 +42,7 @@ public class SubmitTradeHandler
|
||||
|
||||
public async Task<Guid> HandleAsync(SubmitTradeCommand command, CancellationToken ct = default)
|
||||
{
|
||||
KisTradingHardOff.EnsureDisabled();
|
||||
var trade = Trade.Create(command.SellDecisionId, command.Quantity, command.CorrelationId, _clock.UtcNow.UtcDateTime);
|
||||
await _sql.InsertTradeAsync(trade, ct);
|
||||
_logger.LogInformation("Created trade: {TradeId}", trade.Id);
|
||||
@@ -127,6 +128,7 @@ public class PollTradeStatusHandler
|
||||
|
||||
public async Task HandleAsync(PollTradeStatusCommand command, CancellationToken ct = default)
|
||||
{
|
||||
KisTradingHardOff.EnsureDisabled();
|
||||
var trade = await _sql.GetTradeByIdAsync(command.TradeId, command.CorrelationId, ct);
|
||||
if (trade == null)
|
||||
{
|
||||
@@ -222,6 +224,7 @@ public class ConfirmSettlementHandler
|
||||
|
||||
public async Task HandleAsync(ConfirmSettlementCommand command, CancellationToken ct = default)
|
||||
{
|
||||
KisTradingHardOff.EnsureDisabled();
|
||||
var trade = await _sql.GetTradeByIdAsync(command.TradeId, command.CorrelationId, ct);
|
||||
if (trade == null)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
using System.Net;
|
||||
using KArtSell.Modules.ModelOperations.TradeExecution;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Xunit;
|
||||
|
||||
namespace KArtSell.Integration.Tests.TradeExecution;
|
||||
|
||||
public sealed class KisTradingHardOffTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Every_kis_trade_operation_fails_before_any_http_call()
|
||||
{
|
||||
var handler = new CountingHandler();
|
||||
var service = new KisTradeExecutionService(new HttpClient(handler), NullLogger<KisTradeExecutionService>.Instance);
|
||||
var correlationId = Guid.NewGuid();
|
||||
|
||||
await Assert.ThrowsAsync<KisTradingDisabledException>(() => service.ExecuteTradeAsync(Guid.NewGuid(), 1, 1m, correlationId));
|
||||
await Assert.ThrowsAsync<KisTradingDisabledException>(() => service.GetOrderStatusAsync("order", correlationId));
|
||||
await Assert.ThrowsAsync<KisTradingDisabledException>(() => service.CancelOrderAsync("order", "test", correlationId));
|
||||
await Assert.ThrowsAsync<KisTradingDisabledException>(() => service.ConfirmSettlementAsync("order", correlationId));
|
||||
|
||||
Assert.Equal(0, handler.CallCount);
|
||||
}
|
||||
|
||||
private sealed class CountingHandler : HttpMessageHandler
|
||||
{
|
||||
public int CallCount { get; private set; }
|
||||
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
CallCount++;
|
||||
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user