실거래 경계를 열기 위한 domain, contract, API, worker, client parser 등록을 함께 추가한다. 브로커 capability 조회와 live trading capability 광고가 각 런타임에서 일관되게 검증되도록 테스트와 review artifact를 포함한다.
214 lines
6.8 KiB
Go
214 lines
6.8 KiB
Go
package socket
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"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"
|
|
protoSocket "git.toki-labs.com/toki/proto-socket/go"
|
|
)
|
|
|
|
type fakeLiveService struct {
|
|
cap trading.BrokerCapability
|
|
err error
|
|
}
|
|
|
|
func (f *fakeLiveService) GetLiveBrokerCapabilities(ctx context.Context, accountID string) (trading.BrokerCapability, error) {
|
|
return f.cap, f.err
|
|
}
|
|
|
|
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{err: 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")
|
|
}
|
|
}
|