실거래 주문 생성/조회/취소 흐름이 contracts, worker, API, CLI, Flutter parser까지 같은 메시지 경계로 이어져야 한다.\n\n브로커 호출 전 operator confirmation과 malformed order validation을 worker-owned runtime에서 막고, 리뷰 완료 산출물을 archive에 보존한다.
268 lines
9.4 KiB
Go
268 lines
9.4 KiB
Go
package socket
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"google.golang.org/protobuf/proto"
|
|
|
|
altv1 "git.toki-labs.com/toki/alt/packages/contracts/gen/go/alt/v1"
|
|
apiContracts "git.toki-labs.com/toki/alt/services/api/internal/contracts"
|
|
"git.toki-labs.com/toki/alt/services/api/internal/workerclient"
|
|
protoSocket "git.toki-labs.com/toki/proto-socket/go"
|
|
)
|
|
|
|
func TestHandleGetLiveBrokerCapabilitiesWorkerForward(t *testing.T) {
|
|
expectedCap := &altv1.LiveBrokerCapability{Broker: "kis"}
|
|
worker := &fakeWorkerClient{
|
|
liveCapRes: &altv1.GetLiveBrokerCapabilitiesResponse{BrokerCapabilities: expectedCap},
|
|
isConnected: true,
|
|
}
|
|
|
|
resp, err := handleGetLiveBrokerCapabilities(worker, &altv1.GetLiveBrokerCapabilitiesRequest{AccountId: "test"})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if resp.GetError() != nil {
|
|
t.Fatalf("unexpected error: %+v", resp.GetError())
|
|
}
|
|
if resp.GetBrokerCapabilities() == nil {
|
|
t.Fatal("expected broker capabilities, got nil")
|
|
}
|
|
if worker.GetLiveBrokerCapabilitiesCallCount() != 1 {
|
|
t.Errorf("expected worker call count 1, got %d", worker.GetLiveBrokerCapabilitiesCallCount())
|
|
}
|
|
if resp.GetBrokerCapabilities().GetBroker() != "kis" {
|
|
t.Errorf("broker mismatch: %q", resp.GetBrokerCapabilities().GetBroker())
|
|
}
|
|
}
|
|
|
|
func TestHandleGetLiveBrokerCapabilitiesNilWorker(t *testing.T) {
|
|
resp, err := handleGetLiveBrokerCapabilities(nil, &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 error code, got %q", resp.GetError().GetCode())
|
|
}
|
|
}
|
|
|
|
func TestHandleGetLiveBrokerCapabilitiesEmptyAccountID(t *testing.T) {
|
|
worker := &fakeWorkerClient{isConnected: true}
|
|
resp, err := handleGetLiveBrokerCapabilities(worker, &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 error 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())
|
|
}
|
|
if worker.GetLiveBrokerCapabilitiesCallCount() != 0 {
|
|
t.Errorf("worker should not be called when account_id is empty, got %d calls", worker.GetLiveBrokerCapabilitiesCallCount())
|
|
}
|
|
}
|
|
|
|
func TestHandleGetLiveBrokerCapabilitiesWorkerUnavailable(t *testing.T) {
|
|
worker := &fakeWorkerClient{connectErr: workerclient.ErrUnavailable, isConnected: false}
|
|
resp, err := handleGetLiveBrokerCapabilities(worker, &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 error code, got %q", resp.GetError().GetCode())
|
|
}
|
|
}
|
|
|
|
func TestHandleGetLiveBrokerCapabilitiesWorkerTimeout(t *testing.T) {
|
|
worker := &fakeWorkerClient{connectErr: workerclient.ErrTimeout, isConnected: false}
|
|
resp, err := handleGetLiveBrokerCapabilities(worker, &altv1.GetLiveBrokerCapabilitiesRequest{AccountId: "test"})
|
|
if err != nil {
|
|
t.Fatalf("expected typed timeout response, got error: %v", err)
|
|
}
|
|
if resp.GetError() == nil {
|
|
t.Fatal("expected error, got nil")
|
|
}
|
|
if resp.GetError().GetCode() != backtestErrorTimeout {
|
|
t.Errorf("expected timeout error code, got %q", resp.GetError().GetCode())
|
|
}
|
|
}
|
|
|
|
// TestHandleGetLiveBrokerCapabilitiesWorkerCallTimeout verifies the live handler
|
|
// maps a worker-level call timeout (GetLiveBrokerCapabilities returning
|
|
// workerclient.ErrTimeout) to backtestErrorTimeout. This is separate from the
|
|
// Connect failure test above because Connect failure path is tested with
|
|
// connectErr: workerclient.ErrTimeout above.
|
|
func TestHandleGetLiveBrokerCapabilitiesWorkerCallTimeout(t *testing.T) {
|
|
worker := &fakeWorkerClient{err: workerclient.ErrTimeout, isConnected: true}
|
|
resp, err := handleGetLiveBrokerCapabilities(worker, &altv1.GetLiveBrokerCapabilitiesRequest{AccountId: "test"})
|
|
if err != nil {
|
|
t.Fatalf("expected typed timeout response, got error: %v", err)
|
|
}
|
|
if resp.GetError() == nil {
|
|
t.Fatal("expected error, got nil")
|
|
}
|
|
if resp.GetError().GetCode() != backtestErrorTimeout {
|
|
t.Errorf("expected timeout error code, got %q", resp.GetError().GetCode())
|
|
}
|
|
if worker.GetLiveBrokerCapabilitiesCallCount() != 1 {
|
|
t.Errorf("expected worker call count 1, got %d", worker.GetLiveBrokerCapabilitiesCallCount())
|
|
}
|
|
}
|
|
|
|
func TestHandleGetLiveBrokerCapabilitiesNilWorkerResponse(t *testing.T) {
|
|
worker := &fakeWorkerClient{liveCapRes: nil, isConnected: true}
|
|
resp, err := handleGetLiveBrokerCapabilities(worker, &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 error code, got %q", resp.GetError().GetCode())
|
|
}
|
|
}
|
|
|
|
func TestSessionHandlersIncludeLive(t *testing.T) {
|
|
worker := &fakeWorkerClient{isConnected: true}
|
|
handlers := sessionHandlers(worker)
|
|
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 TestLiveParserMapContainsLiveMessages(t *testing.T) {
|
|
pm := apiContracts.ParserMap()
|
|
for _, m := range []proto.Message{
|
|
&altv1.GetLiveBrokerCapabilitiesRequest{},
|
|
&altv1.GetLiveBrokerCapabilitiesResponse{},
|
|
&altv1.LiveBrokerCapability{},
|
|
&altv1.SubmitLiveOrderRequest{},
|
|
&altv1.SubmitLiveOrderResponse{},
|
|
&altv1.CancelLiveOrderRequest{},
|
|
&altv1.CancelLiveOrderResponse{},
|
|
&altv1.GetLiveOrderRequest{},
|
|
&altv1.GetLiveOrderResponse{},
|
|
} {
|
|
name := protoSocket.TypeNameOf(m)
|
|
if _, ok := pm[name]; !ok {
|
|
t.Errorf("API parser map missing %q", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHandleSubmitLiveOrderForward(t *testing.T) {
|
|
worker := &fakeWorkerClient{
|
|
liveSubmitRes: &altv1.SubmitLiveOrderResponse{Order: &altv1.LiveOrder{Id: "lo-1", Status: "submitted"}},
|
|
isConnected: true,
|
|
}
|
|
resp, err := handleSubmitLiveOrder(worker, &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 error: %v", err)
|
|
}
|
|
if resp.GetError() != nil {
|
|
t.Fatalf("unexpected typed error: %+v", resp.GetError())
|
|
}
|
|
if resp.GetOrder().GetId() != "lo-1" {
|
|
t.Errorf("unexpected order id: %q", resp.GetOrder().GetId())
|
|
}
|
|
if worker.liveSubmitReq == nil {
|
|
t.Error("expected worker to be called")
|
|
}
|
|
}
|
|
|
|
func TestHandleSubmitLiveOrderMissingAccountID(t *testing.T) {
|
|
worker := &fakeWorkerClient{isConnected: true}
|
|
resp, err := handleSubmitLiveOrder(worker, &altv1.SubmitLiveOrderRequest{
|
|
Intent: &altv1.LiveOrderIntent{InstrumentId: "KRX:005930"},
|
|
})
|
|
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())
|
|
}
|
|
if worker.liveSubmitReq != nil {
|
|
t.Error("worker should not be called when account_id is missing")
|
|
}
|
|
}
|
|
|
|
func TestHandleSubmitLiveOrderNilWorker(t *testing.T) {
|
|
resp, err := handleSubmitLiveOrder(nil, &altv1.SubmitLiveOrderRequest{
|
|
AccountId: "acct-1",
|
|
Intent: &altv1.LiveOrderIntent{InstrumentId: "KRX:005930"},
|
|
})
|
|
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 TestHandleCancelLiveOrderForward(t *testing.T) {
|
|
worker := &fakeWorkerClient{
|
|
liveCancelRes: &altv1.CancelLiveOrderResponse{Order: &altv1.LiveOrder{Id: "lo-1", Status: "canceled"}},
|
|
isConnected: true,
|
|
}
|
|
resp, err := handleCancelLiveOrder(worker, &altv1.CancelLiveOrderRequest{
|
|
AccountId: "acct-1",
|
|
LiveOrderId: "lo-1",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if resp.GetError() != nil {
|
|
t.Fatalf("unexpected typed error: %+v", resp.GetError())
|
|
}
|
|
if resp.GetOrder().GetStatus() != "canceled" {
|
|
t.Errorf("unexpected order status: %q", resp.GetOrder().GetStatus())
|
|
}
|
|
}
|
|
|
|
func TestHandleGetLiveOrderForward(t *testing.T) {
|
|
worker := &fakeWorkerClient{
|
|
liveGetRes: &altv1.GetLiveOrderResponse{Order: &altv1.LiveOrder{Id: "lo-1", Status: "filled", BrokerStatus: "FILLED"}},
|
|
isConnected: true,
|
|
}
|
|
resp, err := handleGetLiveOrder(worker, &altv1.GetLiveOrderRequest{
|
|
AccountId: "acct-1",
|
|
LiveOrderId: "lo-1",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if resp.GetError() != nil {
|
|
t.Fatalf("unexpected typed error: %+v", resp.GetError())
|
|
}
|
|
if resp.GetOrder().GetBrokerStatus() != "FILLED" {
|
|
t.Errorf("expected broker_status FILLED, got %q", resp.GetOrder().GetBrokerStatus())
|
|
}
|
|
}
|