alt/services/worker/internal/socket/live_test.go
toki c0db4b24c1 feat(live-trading): 계좌 동기화와 감사 추적을 추가한다
Live Trading Boundary의 남은 account-sync/audit-trail 작업을 완료해 운영자 headless workflow와 roadmap 완료 후보 상태를 함께 반영한다.
2026-06-08 11:18:41 +09:00

822 lines
26 KiB
Go

package socket
import (
"context"
"errors"
"fmt"
"testing"
"time"
altv1 "git.toki-labs.com/toki/alt/packages/contracts/gen/go/alt/v1"
"git.toki-labs.com/toki/alt/packages/domain/market"
"git.toki-labs.com/toki/alt/packages/domain/trading"
"git.toki-labs.com/toki/alt/services/worker/internal/livetrading"
protoSocket "git.toki-labs.com/toki/proto-socket/go"
)
type fakeLiveService struct {
cap trading.BrokerCapability
capErr error
submitOrder livetrading.LiveOrder
submitErr error
cancelOrder livetrading.LiveOrder
cancelErr error
getOrder livetrading.LiveOrder
getErr error
policy trading.RiskPolicy
killSwitch trading.KillSwitchState
syncSnap trading.AccountSnapshot
syncSnapErr error
getSnap trading.AccountSnapshot
getSnapErr error
listAuditEvents []trading.AuditEvent
listAuditErr error
}
func (f *fakeLiveService) GetLiveBrokerCapabilities(ctx context.Context, accountID string) (trading.BrokerCapability, error) {
return f.cap, f.capErr
}
func (f *fakeLiveService) SubmitLiveOrder(ctx context.Context, req livetrading.SubmitRequest) (livetrading.LiveOrder, error) {
return f.submitOrder, f.submitErr
}
func (f *fakeLiveService) CancelLiveOrder(ctx context.Context, accountID, liveOrderID string) (livetrading.LiveOrder, error) {
return f.cancelOrder, f.cancelErr
}
func (f *fakeLiveService) GetLiveOrder(ctx context.Context, accountID, liveOrderID string) (livetrading.LiveOrder, error) {
return f.getOrder, f.getErr
}
func (f *fakeLiveService) GetRiskPolicy() trading.RiskPolicy {
return f.policy
}
func (f *fakeLiveService) GetKillSwitch() trading.KillSwitchState {
return f.killSwitch
}
func (f *fakeLiveService) SetKillSwitch(ks trading.KillSwitchState) trading.KillSwitchState {
f.killSwitch = ks
return ks
}
func sampleLiveCapability() trading.BrokerCapability {
return trading.BrokerCapability{
Broker: trading.BrokerKIS,
Markets: map[market.Market]bool{market.MarketKR: true},
Venues: map[market.Venue]bool{market.VenueKRX: true},
OrderTypes: []trading.OrderType{trading.OrderTypeMarket, trading.OrderTypeLimit},
SupportsCancel: true,
SupportsAccount: true,
SupportsAuditTags: false,
CheckedAt: time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC),
}
}
func TestHandleGetLiveBrokerCapabilitiesSuccess(t *testing.T) {
svc := &fakeLiveService{cap: sampleLiveCapability()}
deps := Deps{Live: svc}
resp, err := handleGetLiveBrokerCapabilities(deps, &altv1.GetLiveBrokerCapabilitiesRequest{AccountId: "test"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp.GetError() != nil {
t.Fatalf("unexpected typed error: %+v", resp.GetError())
}
cap := resp.GetBrokerCapabilities()
if cap == nil {
t.Fatal("expected broker capabilities in response")
}
if cap.GetBroker() != "kis" {
t.Errorf("broker mismatch: %q", cap.GetBroker())
}
if !cap.GetSupportsCancel() {
t.Error("expected supports_cancel to be true")
}
}
func TestHandleGetLiveBrokerCapabilitiesUnavailable(t *testing.T) {
deps := Deps{}
resp, err := handleGetLiveBrokerCapabilities(deps, &altv1.GetLiveBrokerCapabilitiesRequest{AccountId: "test"})
if err != nil {
t.Fatalf("expected typed unavailable response, got error: %v", err)
}
if resp.GetError() == nil {
t.Fatal("expected error, got nil")
}
if resp.GetError().GetCode() != backtestErrorUnavailable {
t.Errorf("expected unavailable code, got %q", resp.GetError().GetCode())
}
}
func TestHandleGetLiveBrokerCapabilitiesEmptyAccountID(t *testing.T) {
svc := &fakeLiveService{cap: sampleLiveCapability()}
deps := Deps{Live: svc}
resp, err := handleGetLiveBrokerCapabilities(deps, &altv1.GetLiveBrokerCapabilitiesRequest{AccountId: ""})
if err != nil {
t.Fatalf("expected typed invalid_request response, got error: %v", err)
}
if resp.GetError() == nil {
t.Fatal("expected error, got nil")
}
if resp.GetError().GetCode() != backtestErrorInvalidRequest {
t.Errorf("expected invalid_request code, got %q", resp.GetError().GetCode())
}
if resp.GetError().GetMessage() != "invalid live trading request: account_id is required" {
t.Errorf("unexpected error message: %q", resp.GetError().GetMessage())
}
}
func TestHandleGetLiveBrokerCapabilitiesServiceError(t *testing.T) {
svc := &fakeLiveService{capErr: errors.New("broker connection failed")}
deps := Deps{Live: svc}
resp, err := handleGetLiveBrokerCapabilities(deps, &altv1.GetLiveBrokerCapabilitiesRequest{AccountId: "test"})
if err != nil {
t.Fatalf("expected typed internal response, got error: %v", err)
}
if resp.GetError() == nil {
t.Fatal("expected error, got nil")
}
if resp.GetError().GetCode() != backtestErrorInternal {
t.Errorf("expected internal code, got %q", resp.GetError().GetCode())
}
}
func TestLiveHandlersRegistration(t *testing.T) {
registered := make(map[string]bool)
for _, h := range liveHandlers(Deps{}) {
if h.requestType == "" {
t.Error("handler has empty request type")
}
if h.register == nil {
t.Errorf("handler %q has nil register", h.requestType)
}
registered[h.requestType] = true
}
if !registered["alt.v1.GetLiveBrokerCapabilitiesRequest"] {
t.Error("missing live capability handler registration")
}
}
func TestLiveHandlersWithNilLiveService(t *testing.T) {
// liveHandlers should still register even when deps.Live is nil.
// The handler returns unavailable error at runtime.
handlers := liveHandlers(Deps{})
if len(handlers) == 0 {
t.Error("expected live handlers to be registered even with nil Live service")
}
}
func TestLiveSocketUnavailableReturnsTypedError(t *testing.T) {
client := startBacktestWorkerTestClient(t, Deps{})
resp, err := protoSocket.SendRequestTyped[*altv1.GetLiveBrokerCapabilitiesRequest, *altv1.GetLiveBrokerCapabilitiesResponse](
&client.Communicator,
&altv1.GetLiveBrokerCapabilitiesRequest{AccountId: "test"},
500*time.Millisecond,
)
if err != nil {
t.Fatalf("request should return typed error response without timeout: %v", err)
}
if resp.GetError() == nil {
t.Fatal("expected error, got nil")
}
if resp.GetError().GetCode() != backtestErrorUnavailable {
t.Errorf("expected unavailable code, got %q", resp.GetError().GetCode())
}
}
func TestLiveSocketRoundTripWithFakeService(t *testing.T) {
svc := &fakeLiveService{cap: sampleLiveCapability()}
client := startBacktestWorkerTestClient(t, Deps{Live: svc})
resp, err := protoSocket.SendRequestTyped[*altv1.GetLiveBrokerCapabilitiesRequest, *altv1.GetLiveBrokerCapabilitiesResponse](
&client.Communicator,
&altv1.GetLiveBrokerCapabilitiesRequest{AccountId: "test"},
1*time.Second,
)
if err != nil {
t.Fatalf("round trip failed: %v", err)
}
if resp.GetError() != nil {
t.Fatalf("unexpected typed error: %+v", resp.GetError())
}
cap := resp.GetBrokerCapabilities()
if cap == nil {
t.Fatal("expected broker capabilities in response")
}
if cap.GetBroker() != "kis" {
t.Errorf("broker mismatch: %q", cap.GetBroker())
}
}
func TestLiveSocketEmptyAccountIDReturnsInvalidRequest(t *testing.T) {
svc := &fakeLiveService{cap: sampleLiveCapability()}
client := startBacktestWorkerTestClient(t, Deps{Live: svc})
resp, err := protoSocket.SendRequestTyped[*altv1.GetLiveBrokerCapabilitiesRequest, *altv1.GetLiveBrokerCapabilitiesResponse](
&client.Communicator,
&altv1.GetLiveBrokerCapabilitiesRequest{AccountId: ""},
500*time.Millisecond,
)
if err != nil {
t.Fatalf("request should return typed error response without timeout: %v", err)
}
if resp.GetError() == nil {
t.Fatal("expected error, got nil")
}
if resp.GetError().GetCode() != backtestErrorInvalidRequest {
t.Errorf("expected invalid_request code, got %q", resp.GetError().GetCode())
}
if resp.GetBrokerCapabilities() != nil {
t.Error("expected no broker capabilities when account_id is empty")
}
}
func TestSessionHandlersIncludeLive(t *testing.T) {
handlers := sessionHandlers(Deps{})
seenLive := false
for _, h := range handlers {
if h.requestType == "alt.v1.GetLiveBrokerCapabilitiesRequest" {
seenLive = true
break
}
}
if !seenLive {
t.Error("sessionHandlers should include live capability handler")
}
}
func TestLiveHandlersRegistrationIncludesOrderLifecycle(t *testing.T) {
required := map[string]bool{
"alt.v1.GetLiveBrokerCapabilitiesRequest": false,
"alt.v1.SubmitLiveOrderRequest": false,
"alt.v1.CancelLiveOrderRequest": false,
"alt.v1.GetLiveOrderRequest": false,
}
for _, h := range liveHandlers(Deps{}) {
if _, ok := required[h.requestType]; ok {
required[h.requestType] = true
}
}
for name, seen := range required {
if !seen {
t.Errorf("missing live handler registration: %s", name)
}
}
}
func TestHandleSubmitLiveOrderConfirmationRequired(t *testing.T) {
svc := &fakeLiveService{submitErr: livetrading.ErrConfirmationRequired}
deps := Deps{Live: svc}
resp, err := handleSubmitLiveOrder(deps, &altv1.SubmitLiveOrderRequest{
AccountId: "acct-1",
Intent: &altv1.LiveOrderIntent{InstrumentId: "KRX:005930", Side: "buy", Type: "market"},
OperatorConfirmation: &altv1.OperatorConfirmation{Confirmed: false},
})
if err != nil {
t.Fatalf("unexpected go error: %v", err)
}
if resp.GetError() == nil {
t.Fatal("expected typed error, got nil")
}
if resp.GetError().GetCode() != backtestErrorInvalidRequest {
t.Errorf("expected invalid_request, got %q", resp.GetError().GetCode())
}
}
func TestHandleSubmitLiveOrderUnavailable(t *testing.T) {
deps := Deps{}
resp, err := handleSubmitLiveOrder(deps, &altv1.SubmitLiveOrderRequest{
AccountId: "acct-1",
Intent: &altv1.LiveOrderIntent{InstrumentId: "KRX:005930", Side: "buy", Type: "market"},
OperatorConfirmation: &altv1.OperatorConfirmation{Confirmed: true},
})
if err != nil {
t.Fatalf("unexpected go error: %v", err)
}
if resp.GetError() == nil {
t.Fatal("expected typed error, got nil")
}
if resp.GetError().GetCode() != backtestErrorUnavailable {
t.Errorf("expected unavailable, got %q", resp.GetError().GetCode())
}
}
func TestHandleSubmitLiveOrderSuccess(t *testing.T) {
svc := &fakeLiveService{
submitOrder: livetrading.LiveOrder{
ID: "lo-acct-1-1",
AccountID: "acct-1",
Status: "submitted",
BrokerStatus: "ACCEPTED",
BrokerOrderID: "broker-001",
Intent: trading.OrderIntent{
InstrumentID: "KRX:005930",
Type: "kis_best_limit",
},
},
}
deps := Deps{Live: svc}
resp, err := handleSubmitLiveOrder(deps, &altv1.SubmitLiveOrderRequest{
AccountId: "acct-1",
Intent: &altv1.LiveOrderIntent{InstrumentId: "KRX:005930", Side: "buy", Type: "kis_best_limit"},
OperatorConfirmation: &altv1.OperatorConfirmation{Confirmed: true, OperatorId: "op-1"},
})
if err != nil {
t.Fatalf("unexpected go error: %v", err)
}
if resp.GetError() != nil {
t.Fatalf("unexpected typed error: %+v", resp.GetError())
}
if resp.GetOrder().GetId() != "lo-acct-1-1" {
t.Errorf("unexpected order id: %q", resp.GetOrder().GetId())
}
if resp.GetOrder().GetType() != "kis_best_limit" {
t.Errorf("order type not preserved: %q", resp.GetOrder().GetType())
}
}
func TestHandleCancelLiveOrderNotFound(t *testing.T) {
svc := &fakeLiveService{cancelErr: livetrading.ErrOrderNotFound}
deps := Deps{Live: svc}
resp, err := handleCancelLiveOrder(deps, &altv1.CancelLiveOrderRequest{
AccountId: "acct-1",
LiveOrderId: "nonexistent",
})
if err != nil {
t.Fatalf("unexpected go error: %v", err)
}
if resp.GetError() == nil {
t.Fatal("expected typed error, got nil")
}
if resp.GetError().GetCode() != backtestErrorNotFound {
t.Errorf("expected not_found, got %q", resp.GetError().GetCode())
}
}
func TestHandleGetLiveOrderSuccess(t *testing.T) {
svc := &fakeLiveService{
getOrder: livetrading.LiveOrder{
ID: "lo-acct-1-1",
AccountID: "acct-1",
Status: "filled",
BrokerStatus: "FILLED",
},
}
deps := Deps{Live: svc}
resp, err := handleGetLiveOrder(deps, &altv1.GetLiveOrderRequest{
AccountId: "acct-1",
LiveOrderId: "lo-acct-1-1",
})
if err != nil {
t.Fatalf("unexpected go error: %v", err)
}
if resp.GetError() != nil {
t.Fatalf("unexpected typed error: %+v", resp.GetError())
}
if resp.GetOrder().GetStatus() != "filled" {
t.Errorf("expected status filled, got %q", resp.GetOrder().GetStatus())
}
if resp.GetOrder().GetBrokerStatus() != "FILLED" {
t.Errorf("expected broker_status FILLED, got %q", resp.GetOrder().GetBrokerStatus())
}
}
func TestHandleSubmitLiveOrderRiskBlockedReturnsInvalidRequest(t *testing.T) {
svc := &fakeLiveService{submitErr: livetrading.ErrRiskBlocked}
deps := Deps{Live: svc}
resp, err := handleSubmitLiveOrder(deps, &altv1.SubmitLiveOrderRequest{
AccountId: "acct-1",
Intent: &altv1.LiveOrderIntent{InstrumentId: "KRX:005930", Side: "buy", Type: "market"},
OperatorConfirmation: &altv1.OperatorConfirmation{Confirmed: true},
})
if err != nil {
t.Fatalf("unexpected go error: %v", err)
}
if resp.GetError() == nil {
t.Fatal("expected typed error, got nil")
}
if resp.GetError().GetCode() != backtestErrorInvalidRequest {
t.Errorf("expected invalid_request for risk-blocked, got %q", resp.GetError().GetCode())
}
}
func TestHandleSubmitLiveOrderLimitPriceCurrencyRoundTrip(t *testing.T) {
svc := &fakeLiveService{
submitOrder: livetrading.LiveOrder{
ID: "lo-acct-1-1",
AccountID: "acct-1",
Status: "submitted",
BrokerStatus: "ACCEPTED",
BrokerOrderID: "broker-001",
Intent: trading.OrderIntent{
InstrumentID: "KRX:005930",
Type: "limit",
LimitPrice: market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "75000"}},
},
},
}
deps := Deps{Live: svc}
resp, err := handleSubmitLiveOrder(deps, &altv1.SubmitLiveOrderRequest{
AccountId: "acct-1",
Intent: &altv1.LiveOrderIntent{
InstrumentId: "KRX:005930",
Side: "buy",
Type: "limit",
LimitPrice: &altv1.Price{Currency: altv1.Currency_CURRENCY_KRW, Amount: &altv1.Decimal{Value: "75000"}},
},
OperatorConfirmation: &altv1.OperatorConfirmation{Confirmed: true, OperatorId: "op-1"},
})
if err != nil {
t.Fatalf("unexpected go error: %v", err)
}
if resp.GetError() != nil {
t.Fatalf("unexpected typed error: %+v", resp.GetError())
}
lp := resp.GetOrder().GetLimitPrice()
if lp == nil {
t.Fatal("expected limit_price in response")
}
if lp.GetCurrency() != altv1.Currency_CURRENCY_KRW {
t.Errorf("expected CURRENCY_KRW in response, got %q", lp.GetCurrency())
}
if lp.GetAmount().GetValue() != "75000" {
t.Errorf("expected limit_price amount 75000, got %q", lp.GetAmount().GetValue())
}
}
func TestHandleSubmitLiveOrderRiskBlockedIncludesOrderAndDecision(t *testing.T) {
rejOrder := livetrading.LiveOrder{
ID: "lo-acct-1-1",
AccountID: "acct-1",
Status: trading.OrderStatusRejected,
RejectionReason: "kill switch is active",
}
svc := &fakeLiveService{
submitOrder: rejOrder,
submitErr: fmt.Errorf("%w: kill switch is active", livetrading.ErrRiskBlocked),
}
deps := Deps{Live: svc}
resp, err := handleSubmitLiveOrder(deps, &altv1.SubmitLiveOrderRequest{
AccountId: "acct-1",
Intent: &altv1.LiveOrderIntent{InstrumentId: "KRX:005930", Side: "buy", Type: "market"},
OperatorConfirmation: &altv1.OperatorConfirmation{Confirmed: true},
})
if err != nil {
t.Fatalf("unexpected go error: %v", err)
}
if resp.GetError() == nil {
t.Fatal("expected typed error for risk-blocked, got nil")
}
if resp.GetError().GetCode() != backtestErrorInvalidRequest {
t.Errorf("expected invalid_request, got %q", resp.GetError().GetCode())
}
if resp.GetOrder() == nil {
t.Fatal("expected rejected order in response, got nil")
}
if resp.GetOrder().GetId() != "lo-acct-1-1" {
t.Errorf("unexpected order id: %q", resp.GetOrder().GetId())
}
if resp.GetRiskDecision() == nil {
t.Fatal("expected risk decision in response, got nil")
}
if resp.GetRiskDecision().GetAllowed() {
t.Error("expected risk decision allowed=false")
}
if resp.GetRiskDecision().GetReason() != "kill switch is active" {
t.Errorf("unexpected risk decision reason: %q", resp.GetRiskDecision().GetReason())
}
}
func TestHandleSubmitLiveOrderInvalidCurrencyReturnsInvalidRequest(t *testing.T) {
svc := &fakeLiveService{}
deps := Deps{Live: svc}
resp, err := handleSubmitLiveOrder(deps, &altv1.SubmitLiveOrderRequest{
AccountId: "acct-1",
Intent: &altv1.LiveOrderIntent{
InstrumentId: "KRX:005930",
Side: "buy",
Type: "limit",
LimitPrice: &altv1.Price{Currency: altv1.Currency_CURRENCY_UNSPECIFIED, Amount: &altv1.Decimal{Value: "75000"}},
},
OperatorConfirmation: &altv1.OperatorConfirmation{Confirmed: true, OperatorId: "op-1"},
})
if err != nil {
t.Fatalf("unexpected go error: %v", err)
}
if resp.GetError() == nil {
t.Fatal("expected typed error for unsupported currency, got nil")
}
if resp.GetError().GetCode() != backtestErrorInvalidRequest {
t.Errorf("expected invalid_request, got %q", resp.GetError().GetCode())
}
}
func TestSubmitLiveOrderSocketRoundTrip(t *testing.T) {
svc := &fakeLiveService{
submitOrder: livetrading.LiveOrder{
ID: "lo-acct-1-1",
AccountID: "acct-1",
Status: "submitted",
BrokerStatus: "ACCEPTED",
BrokerOrderID: "broker-001",
Intent: trading.OrderIntent{InstrumentID: "KRX:005930", Type: "market"},
},
}
client := startBacktestWorkerTestClient(t, Deps{Live: svc})
resp, err := protoSocket.SendRequestTyped[*altv1.SubmitLiveOrderRequest, *altv1.SubmitLiveOrderResponse](
&client.Communicator,
&altv1.SubmitLiveOrderRequest{
AccountId: "acct-1",
Intent: &altv1.LiveOrderIntent{InstrumentId: "KRX:005930", Side: "buy", Type: "market"},
OperatorConfirmation: &altv1.OperatorConfirmation{Confirmed: true, OperatorId: "op-1"},
},
1*time.Second,
)
if err != nil {
t.Fatalf("round trip failed: %v", err)
}
if resp.GetError() != nil {
t.Fatalf("unexpected typed error: %+v", resp.GetError())
}
if resp.GetOrder().GetId() != "lo-acct-1-1" {
t.Errorf("unexpected order id: %q", resp.GetOrder().GetId())
}
}
func (f *fakeLiveService) SyncLiveAccount(ctx context.Context, accountID string) (trading.AccountSnapshot, error) {
return f.syncSnap, f.syncSnapErr
}
func (f *fakeLiveService) GetLiveAccountSnapshot(ctx context.Context, accountID string) (trading.AccountSnapshot, error) {
return f.getSnap, f.getSnapErr
}
func (f *fakeLiveService) ListLiveAuditEvents(_ context.Context, _ trading.AuditFilter) ([]trading.AuditEvent, error) {
return f.listAuditEvents, f.listAuditErr
}
func sampleAccountSnapshotForSocket() trading.AccountSnapshot {
return trading.AccountSnapshot{
Broker: trading.BrokerKIS,
AccountID: "op-alias-001",
Cash: []trading.CashBalance{
{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "5000000"}},
},
Positions: []trading.PositionSnapshot{
{
InstrumentID: "KRX:005930",
Side: trading.OrderSideBuy,
Quantity: market.Quantity{Amount: market.Decimal{Value: "10"}},
},
},
Stale: false,
}
}
func TestHandleSyncLiveAccountSuccess(t *testing.T) {
snap := sampleAccountSnapshotForSocket()
svc := &fakeLiveService{syncSnap: snap}
deps := Deps{Live: svc}
resp, err := handleSyncLiveAccount(deps, &altv1.SyncLiveAccountRequest{AccountId: "op-alias-001"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp.GetError() != nil {
t.Fatalf("unexpected typed error: %+v", resp.GetError())
}
s := resp.GetSnapshot()
if s == nil {
t.Fatal("expected snapshot in response")
}
if s.GetAccountId() != "op-alias-001" {
t.Errorf("account_id mismatch: %q", s.GetAccountId())
}
if s.GetBroker() != "kis" {
t.Errorf("broker mismatch: %q", s.GetBroker())
}
if len(s.GetCash()) != 1 {
t.Errorf("expected 1 cash entry, got %d", len(s.GetCash()))
}
if s.GetStale() {
t.Error("expected stale=false")
}
}
func TestHandleSyncLiveAccountEmptyAccountIDReturnsInvalidRequest(t *testing.T) {
svc := &fakeLiveService{}
deps := Deps{Live: svc}
resp, err := handleSyncLiveAccount(deps, &altv1.SyncLiveAccountRequest{AccountId: ""})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp.GetError() == nil {
t.Fatal("expected typed error, got nil")
}
if resp.GetError().GetCode() != backtestErrorInvalidRequest {
t.Errorf("expected invalid_request, got %q", resp.GetError().GetCode())
}
}
func TestHandleSyncLiveAccountNilServiceReturnsUnavailable(t *testing.T) {
resp, err := handleSyncLiveAccount(Deps{}, &altv1.SyncLiveAccountRequest{AccountId: "op-alias-001"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp.GetError() == nil {
t.Fatal("expected typed error, got nil")
}
if resp.GetError().GetCode() != backtestErrorUnavailable {
t.Errorf("expected unavailable, got %q", resp.GetError().GetCode())
}
}
func TestHandleGetLiveAccountSnapshotNotFound(t *testing.T) {
svc := &fakeLiveService{getSnapErr: livetrading.ErrAccountSnapshotNotFound}
deps := Deps{Live: svc}
resp, err := handleGetLiveAccountSnapshot(deps, &altv1.GetLiveAccountSnapshotRequest{AccountId: "op-alias-001"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp.GetError() == nil {
t.Fatal("expected typed error, got nil")
}
if resp.GetError().GetCode() != backtestErrorNotFound {
t.Errorf("expected not_found, got %q", resp.GetError().GetCode())
}
}
func TestHandleGetLiveAccountSnapshotSuccess(t *testing.T) {
snap := sampleAccountSnapshotForSocket()
svc := &fakeLiveService{getSnap: snap}
deps := Deps{Live: svc}
resp, err := handleGetLiveAccountSnapshot(deps, &altv1.GetLiveAccountSnapshotRequest{AccountId: "op-alias-001"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp.GetError() != nil {
t.Fatalf("unexpected typed error: %+v", resp.GetError())
}
if resp.GetSnapshot() == nil {
t.Fatal("expected snapshot in response")
}
if resp.GetSnapshot().GetAccountId() != "op-alias-001" {
t.Errorf("account_id mismatch: %q", resp.GetSnapshot().GetAccountId())
}
}
func TestLiveHandlersRegistrationIncludesAccountSync(t *testing.T) {
required := map[string]bool{
"alt.v1.SyncLiveAccountRequest": false,
"alt.v1.GetLiveAccountSnapshotRequest": false,
}
for _, h := range liveHandlers(Deps{}) {
if _, ok := required[h.requestType]; ok {
required[h.requestType] = true
}
}
for name, seen := range required {
if !seen {
t.Errorf("missing live handler registration: %s", name)
}
}
}
func TestLiveHandlersRegistrationIncludesAuditQuery(t *testing.T) {
found := false
for _, h := range liveHandlers(Deps{}) {
if h.requestType == "alt.v1.ListLiveAuditEventsRequest" {
found = true
break
}
}
if !found {
t.Error("missing live handler registration: alt.v1.ListLiveAuditEventsRequest")
}
}
func TestHandleListLiveAuditEventsSuccess(t *testing.T) {
events := []trading.AuditEvent{
{
EventID: "aud-1",
Type: trading.AuditEventTypeSubmitConfirmed,
Timestamp: time.Now().UTC(),
AccountID: "acct-1",
OrderID: "lo-acct-1-1",
Status: "submitted",
Actor: "op-001",
},
{
EventID: "aud-2",
Type: trading.AuditEventTypeRiskBlocked,
Timestamp: time.Now().UTC(),
AccountID: "acct-1",
Status: "blocked",
},
}
svc := &fakeLiveService{listAuditEvents: events}
deps := Deps{Live: svc}
resp, err := handleListLiveAuditEvents(deps, &altv1.ListLiveAuditEventsRequest{AccountId: "acct-1"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp.GetError() != nil {
t.Fatalf("unexpected typed error: %+v", resp.GetError())
}
if len(resp.GetEvents()) != 2 {
t.Fatalf("expected 2 events, got %d", len(resp.GetEvents()))
}
if resp.GetEvents()[0].GetEventId() != "aud-1" {
t.Errorf("event_id mismatch: %q", resp.GetEvents()[0].GetEventId())
}
if resp.GetEvents()[0].GetType() != string(trading.AuditEventTypeSubmitConfirmed) {
t.Errorf("type mismatch: %q", resp.GetEvents()[0].GetType())
}
if resp.GetEvents()[0].GetActor() != "op-001" {
t.Errorf("actor mismatch: %q", resp.GetEvents()[0].GetActor())
}
}
func TestHandleListLiveAuditEventsEmptyAccountIDReturnsInvalidRequest(t *testing.T) {
svc := &fakeLiveService{}
deps := Deps{Live: svc}
resp, err := handleListLiveAuditEvents(deps, &altv1.ListLiveAuditEventsRequest{AccountId: ""})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp.GetError() == nil {
t.Fatal("expected typed error, got nil")
}
if resp.GetError().GetCode() != backtestErrorInvalidRequest {
t.Errorf("expected invalid_request, got %q", resp.GetError().GetCode())
}
}
func TestHandleListLiveAuditEventsNilServiceReturnsUnavailable(t *testing.T) {
resp, err := handleListLiveAuditEvents(Deps{}, &altv1.ListLiveAuditEventsRequest{AccountId: "acct-1"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp.GetError() == nil {
t.Fatal("expected typed error, got nil")
}
if resp.GetError().GetCode() != backtestErrorUnavailable {
t.Errorf("expected unavailable, got %q", resp.GetError().GetCode())
}
}
func TestHandleListLiveAuditEventsServiceErrorReturnsInternal(t *testing.T) {
svc := &fakeLiveService{listAuditErr: errors.New("store unavailable")}
deps := Deps{Live: svc}
resp, err := handleListLiveAuditEvents(deps, &altv1.ListLiveAuditEventsRequest{AccountId: "acct-1"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp.GetError() == nil {
t.Fatal("expected typed error, got nil")
}
if resp.GetError().GetCode() != backtestErrorInternal {
t.Errorf("expected internal, got %q", resp.GetError().GetCode())
}
}
func TestHandleListLiveAuditEventsEmptyResultIsOK(t *testing.T) {
svc := &fakeLiveService{listAuditEvents: nil}
deps := Deps{Live: svc}
resp, err := handleListLiveAuditEvents(deps, &altv1.ListLiveAuditEventsRequest{AccountId: "acct-1"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp.GetError() != nil {
t.Fatalf("unexpected typed error: %+v", resp.GetError())
}
if len(resp.GetEvents()) != 0 {
t.Errorf("expected empty events, got %d", len(resp.GetEvents()))
}
}