register_test.go
Rama main · réplica de lectura del repositorio original.
// Copyright The Pit Project Owners. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Please see https://officer.openpit.dev and the OWNERS file for details.
package officer
import (
"bytes"
"context"
"crypto/ed25519"
"crypto/sha256"
"database/sql"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"log/slog"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"runtime"
"slices"
"strings"
"syscall"
"testing"
"testing/fstest"
"time"
sdkmcp "github.com/modelcontextprotocol/go-sdk/mcp"
openpit "go.openpit.dev/openpit"
"go.openpit.dev/openpit/accountadjustment"
"go.openpit.dev/openpit/asyncengine"
"go.openpit.dev/openpit/configure"
"go.openpit.dev/openpit/model"
"go.openpit.dev/openpit/param"
"go.openpit.dev/openpit/pretrade"
"go.openpit.dev/openpit/pretrade/policies"
"go.openpit.dev/openpit/reject"
frameworkapp "go.openpit.dev/officer/framework/app"
"go.openpit.dev/officer/framework/auth"
"go.openpit.dev/officer/framework/backend"
"go.openpit.dev/officer/framework/backup"
"go.openpit.dev/officer/framework/domain"
"go.openpit.dev/officer/framework/engine"
"go.openpit.dev/officer/framework/marketdata"
frameworkmcp "go.openpit.dev/officer/framework/mcp"
"go.openpit.dev/officer/framework/node"
"go.openpit.dev/officer/framework/secret"
fwsigning "go.openpit.dev/officer/framework/signing"
"go.openpit.dev/officer/framework/store"
httpx "go.openpit.dev/officer/framework/web/httpapi"
"go.openpit.dev/officer/httpapi"
"go.openpit.dev/officer/internal/access"
"go.openpit.dev/officer/internal/access/accesstest"
"go.openpit.dev/officer/internal/accountmode"
"go.openpit.dev/officer/internal/apitest"
"go.openpit.dev/officer/internal/nonrelease"
"go.openpit.dev/officer/internal/store/sqlite"
"go.openpit.dev/officer/mcptools"
)
type venueReportEngine struct {
*fakeEngine
async *asyncengine.AsyncEngine
}
func (e *venueReportEngine) AsyncEngine() *asyncengine.AsyncEngine {
return e.async
}
func (*venueReportEngine) AccountID(domain.AccountID) (param.AccountID, error) {
return param.NewAccountIDFromUint64(1), nil
}
type linkBeforeReportNode struct {
node.Node
realm store.RealmStore
connection domain.ExternalID
}
func (n *linkBeforeReportNode) ApplyExecutionReport(ctx context.Context, in domain.ExecutionReportInput, caller domain.Caller) (engine.ExecutionReportResult, error) {
if _, err := n.realm.CreateVenueOrder(ctx, domain.VenueOrder{
Order: in.Order, Connection: n.connection, ClientOrderID: "client-order-1", Route: "{}",
}); err != nil {
return engine.ExecutionReportResult{}, err
}
return n.Node.ApplyExecutionReport(ctx, in, caller)
}
func TestManualReportRefusesVenueLinkCreatedAfterServiceEntry(t *testing.T) {
ctx := context.Background()
st, err := sqlite.New(filepath.Join(t.TempDir(), "officer.db"), domain.DefaultRealm)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = st.Close() })
if err := st.Migrate(ctx); err != nil {
t.Fatal(err)
}
driver, err := openpit.NewEngineBuilder().AccountSync().Builtin(policies.BuildOrderValidation()).Build()
if err != nil {
t.Fatal(err)
}
async, err := asyncengine.NewBuilder(driver).WithStopUnderlying(driver.Stop).Dynamic().MaxQueues(0).Build()
if err != nil {
driver.Stop()
t.Fatal(err)
}
t.Cleanup(func() { _ = async.StopGraceful(ctx) })
n, _, err := node.NewLocalNode(ctx, domain.DefaultRealm, st, func(engine.Snapshot) (engine.Engine, error) {
return &venueReportEngine{fakeEngine: &fakeEngine{running: true, sink: &fakeSink{}}, async: async}, nil
}, failOnFatal(t))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = n.Close() })
realm, err := st.ForRealm(ctx, domain.DefaultRealm)
if err != nil {
t.Fatal(err)
}
caller := domain.Caller{Source: domain.SourceAPI, Principal: "operator"}
for _, code := range []string{"AAPL", "USD"} {
if _, err := realm.CreateAsset(ctx, domain.Asset{Code: code}); err != nil {
t.Fatal(err)
}
}
if _, err := realm.CreateAccount(ctx, domain.Account{Code: "acc-1"}); err != nil {
t.Fatal(err)
}
order, err := realm.CreateOrder(ctx, domain.Order{
Account: "acc-1", Source: caller.Source, Principal: caller.Principal,
BaseAsset: "AAPL", QuoteAsset: "USD", Side: domain.OrderSideBuy,
AmountKind: domain.OrderAmountKindQuantity, AmountValue: "1",
Status: domain.OrderStatusSubmitted, DropCopy: true,
})
if err != nil {
t.Fatal(err)
}
connection, err := realm.CreateTradingConnection(ctx, domain.TradingConnection{
Provider: "venue", Label: "Paper", Mode: domain.TradingModePaper,
VenueIdentity: "venue-1", Credentials: "{}",
})
if err != nil {
t.Fatal(err)
}
s, err := backend.New(&linkBeforeReportNode{Node: n, realm: realm, connection: connection.ExternalID}, nil, nil, func([]byte, domain.Order) (string, error) {
return "", errUnexpectedOrderChain
})
if err != nil {
t.Fatal(err)
}
_, _, err = s.ApplyExecutionReport(auth.ContextWithCaller(ctx, caller), domain.ExecutionReportInput{
Order: order.ExternalID, OrderStatus: domain.OrderStatusAccepted,
})
if !errors.Is(err, domain.ErrVenueOrderManaged) {
t.Fatalf("manual report after venue link = %v", err)
}
if _, linked, err := realm.GetVenueOrder(ctx, order.ExternalID); err != nil || !linked {
t.Fatalf("interleaving did not create venue link: %v", err)
}
detail, err := realm.GetOrder(ctx, order.ExternalID)
if err != nil || detail.Order.Status != order.Status || len(detail.Events) != 0 || len(detail.Trades) != 0 {
t.Fatalf("refused manual report changed order: %v", err)
}
}
func TestRegisterTradingComposition(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
path := filepath.Join(t.TempDir(), "officer.db")
st, err := sqlite.New(path, domain.DefaultRealm)
if err != nil {
t.Fatal(err)
}
if err := st.Migrate(ctx); err != nil {
_ = st.Close()
t.Fatal(err)
}
realm, err := st.ForRealm(ctx, domain.DefaultRealm)
if err != nil {
_ = st.Close()
t.Fatal(err)
}
// Invalid credentials exercise the registered constructor without
// network I/O.
_, err = realm.CreateTradingConnection(ctx, domain.TradingConnection{
Provider: domain.TradingProviderAlpaca, Label: "paper", Mode: domain.TradingModePaper, VenueIdentity: "paper-account",
Credentials: "{}", Enabled: true,
})
if err != nil {
_ = st.Close()
t.Fatal(err)
}
if err := st.Close(); err != nil {
t.Fatal(err)
}
builder := frameworkapp.NewBuilder()
if err := Register(builder, Config{SQLitePath: path}); err != nil {
t.Fatal(err)
}
builder.SetNodeBuilder(fakeEngineNodeBuilder)
var logs bytes.Buffer
app, err := builder.Build(ctx, slog.New(slog.NewTextHandler(&logs, nil)), failOnFatal(t))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = app.Close() })
status, err := app.Service().GetTrading(
auth.ContextWithCaller(ctx, auth.SystemCaller()),
)
if err != nil {
t.Fatal(err)
}
if len(status.Providers) != 4 ||
status.Providers[0].Type != domain.TradingProviderAlpaca ||
status.Providers[1].Type != domain.TradingProviderBinance ||
status.Providers[2].Type != domain.TradingProviderEtoro ||
status.Providers[3].Type != domain.TradingProviderIB ||
len(status.Connections) != 1 || status.Connections[0].State != "unavailable" {
t.Fatal("trading runtime was not wired into the control plane before Start")
}
cancel()
if err := app.Close(); err != nil {
t.Fatal(err)
}
if !strings.Contains(logs.String(), "alpaca: apiKey is required") {
t.Fatalf("registered Alpaca constructor was not called: %s", logs.String())
}
}
func TestRegisterBackupBuildIdentity(t *testing.T) {
app, _, _ := buildTestApp(t, nil, nil)
ctx := auth.ContextWithCaller(context.Background(), auth.SystemCaller())
archive, _, err := app.Service().ExportBackup(ctx, backup.Scope{All: true})
if err != nil {
t.Fatal(err)
}
if archive.Manifest.NonRelease != !backend.ReleaseBuild() {
t.Fatalf("backup non-release = %t, release build = %t", archive.Manifest.NonRelease, backend.ReleaseBuild())
}
}
func TestRegisterBuildUsesPopulatedMCPCatalog(t *testing.T) {
ctx := auth.ContextWithCaller(context.Background(), auth.SystemCaller())
builder := frameworkapp.NewBuilder()
if err := Register(
builder, Config{SQLitePath: filepath.Join(t.TempDir(), "officer.db")},
); err != nil {
t.Fatalf("Register: %v", err)
}
builder.SetNodeBuilder(fakeEngineNodeBuilder)
app, err := builder.Build(ctx, slog.Default(), failOnFatal(t))
if err != nil {
t.Fatalf("Build: %v", err)
}
t.Cleanup(func() { _ = app.Close() })
commands, err := app.Service().ListMcpAccess(ctx)
if err != nil {
t.Fatalf("ListMcpAccess: %v", err)
}
expected := frameworkmcp.NewToolRegistry()
mcptools.RegisterTools(expected, nil)
if len(commands) != len(expected.Descriptors()) {
t.Fatalf("ListMcpAccess returned %d commands, want %d", len(commands), len(expected.Descriptors()))
}
getAccount, ok := findCommand(commands, "get_account_state")
if !ok {
t.Fatalf("get_account_state missing from composed catalogue")
}
if !getAccount.Command.DefaultEnabled || !getAccount.Enabled {
t.Fatalf("get_account_state default state = %+v", getAccount)
}
if err := app.Service().SetMcpAccess(ctx, "get_account_state", false); err != nil {
t.Fatalf("SetMcpAccess(get_account_state): %v", err)
}
}
func TestRegisterPassesMasterKeyToSQLite(t *testing.T) {
ctx := context.Background()
path := filepath.Join(t.TempDir(), "officer.db")
key := testMasterKey(t)
builder := frameworkapp.NewBuilder()
if err := Register(builder, Config{SQLitePath: path, MasterKey: &key}); err != nil {
t.Fatalf("Register: %v", err)
}
builder.SetNodeBuilder(fakeEngineNodeBuilder)
app, err := builder.Build(ctx, slog.Default(), failOnFatal(t))
if err != nil {
t.Fatalf("Build with master key: %v", err)
}
if err := app.Close(); err != nil {
t.Fatalf("Close: %v", err)
}
withoutKey := frameworkapp.NewBuilder()
if err := Register(withoutKey, Config{SQLitePath: path}); err != nil {
t.Fatalf("Register without key: %v", err)
}
withoutKey.SetNodeBuilder(fakeEngineNodeBuilder)
_, err = withoutKey.Build(ctx, slog.Default(), failOnFatal(t))
if err == nil {
t.Fatal("Build without the master key returned nil error for a sealed database")
}
if !strings.Contains(err.Error(), "no master key was supplied") {
t.Fatalf("Build without the master key returned the wrong error: %v", err)
}
}
// TestRegisterSigningKeyStorage drives, over REST, the real composition:
// generating and importing a signing key succeed with a master key and
// without one. Without one every private key is stored in the clear, as the
// other stored secrets are; with one every stored private key is sealed.
func TestRegisterSigningKeyStorage(t *testing.T) {
seed := bytes.Repeat([]byte{0x6b}, ed25519.SeedSize)
importBody := fmt.Sprintf(
`{"key":%q,"format":"raw-base64"}`, base64.StdEncoding.EncodeToString(seed),
)
clearPath := filepath.Join(t.TempDir(), "officer.db")
app, handler := serveSeededApp(t, Config{SQLitePath: clearPath}, nil)
accesstest.CompleteOpen(t, handler, accountmode.Request{Mode: accountmode.ModeMulti}, false)
restCall(t, handler, http.MethodPost, "/signing/keys/generate", "", http.StatusCreated)
restCall(t, handler, http.MethodPost, "/signing/keys/import", importBody, http.StatusCreated)
if err := app.Close(); err != nil {
t.Fatalf("Close without a master key: %v", err)
}
clear := storedSigningPrivateKeys(t, clearPath)
if len(clear) != 1 {
t.Fatalf("signing private keys stored without a master key = %d, want 1", len(clear))
}
if !slices.ContainsFunc(clear, func(privateKey []byte) bool {
return bytes.Equal(privateKey, seed)
}) {
t.Fatal("the imported signing private key is not stored as is without a master key")
}
key := testMasterKey(t)
sealedPath := filepath.Join(t.TempDir(), "officer.db")
app, handler = serveSeededApp(t, Config{SQLitePath: sealedPath, MasterKey: &key}, nil)
accesstest.CompleteOpen(t, handler, accountmode.Request{Mode: accountmode.ModeMulti}, true)
restCall(t, handler, http.MethodPost, "/signing/keys/generate", "", http.StatusCreated)
restCall(t, handler, http.MethodPost, "/signing/keys/import", importBody, http.StatusCreated)
if err := app.Close(); err != nil {
t.Fatalf("Close with a master key: %v", err)
}
stored := storedSigningPrivateKeys(t, sealedPath)
if len(stored) != 2 {
t.Fatalf("signing private keys stored with a master key = %d, want 2", len(stored))
}
for _, privateKey := range stored {
if len(privateKey) == ed25519.SeedSize || bytes.Contains(privateKey, seed) {
t.Fatal("a signing private key is stored in the clear")
}
}
}
// storedSigningPrivateKeys reads every non-empty signing_key.private_key of the
// SQLite database at path as stored, past the store and its sealing.
func storedSigningPrivateKeys(t *testing.T, path string) [][]byte {
t.Helper()
db, err := sql.Open("sqlite", path)
if err != nil {
t.Fatalf("open %s: %v", path, err)
}
defer func() { _ = db.Close() }()
rows, err := db.Query(`SELECT private_key FROM signing_key WHERE length(private_key) > 0`)
if err != nil {
t.Fatalf("read signing private keys: %v", err)
}
defer func() { _ = rows.Close() }()
var stored [][]byte
for rows.Next() {
var privateKey []byte
if err := rows.Scan(&privateKey); err != nil {
t.Fatalf("scan signing private key: %v", err)
}
stored = append(stored, privateKey)
}
if err := rows.Err(); err != nil {
t.Fatalf("iterate signing private keys: %v", err)
}
return stored
}
func TestBuildRejectsNodeBuilderWithoutEngine(t *testing.T) {
ctx := context.Background()
builder := frameworkapp.NewBuilder()
if err := Register(
builder, Config{SQLitePath: filepath.Join(t.TempDir(), "officer.db")},
); err != nil {
t.Fatalf("Register: %v", err)
}
var opened store.Store
builder.SetNodeBuilder(func(
buildCtx context.Context, st store.Store, fatalHook frameworkapp.FatalShutdownHook,
) (node.Node, engine.Engine, error) {
opened = st
built, _, err := fakeEngineNodeBuilder(buildCtx, st, fatalHook)
return built, nil, err
})
app, err := builder.Build(ctx, slog.Default(), failOnFatal(t))
if err == nil {
_ = app.Close()
t.Fatal("Build accepted a node hook that returned no engine")
}
if !strings.Contains(err.Error(), "no engine") {
t.Fatalf("Build error = %v, want the missing engine named", err)
}
// Build closed the node, and with it the store the node owns.
if _, err := opened.ForRealm(ctx, domain.DefaultRealm); err == nil ||
!strings.Contains(err.Error(), "sqlite is closed") {
t.Fatalf("ForRealm after the rejected Build = %v, want the store closed", err)
}
}
func TestBuildRejectsMissingAuthorizer(t *testing.T) {
builder := frameworkapp.NewBuilder()
if err := Register(
builder, Config{SQLitePath: filepath.Join(t.TempDir(), "officer.db")},
); err != nil {
t.Fatalf("Register: %v", err)
}
builder.SetNodeBuilder(fakeEngineNodeBuilder)
builder.SetAuthorizer(nil)
app, err := builder.Build(context.Background(), slog.Default(), failOnFatal(t))
if err == nil {
_ = app.Close()
t.Fatal("Build accepted a nil authorizer")
}
if !strings.Contains(err.Error(), "nil authorizer") {
t.Fatalf("Build error = %v, want the missing authorizer named", err)
}
}
func TestBuildRejectsMissingCallerResolver(t *testing.T) {
builder := frameworkapp.NewBuilder()
if err := Register(
builder, Config{SQLitePath: filepath.Join(t.TempDir(), "officer.db")},
); err != nil {
t.Fatalf("Register: %v", err)
}
builder.SetNodeBuilder(fakeEngineNodeBuilder)
builder.SetCallerResolver(nil)
app, err := builder.Build(context.Background(), slog.Default(), failOnFatal(t))
if err == nil {
_ = app.Close()
t.Fatal("Build accepted a nil caller resolver")
}
if !strings.Contains(err.Error(), "nil caller resolver") {
t.Fatalf("Build error = %v, want the missing caller resolver named", err)
}
}
func TestBuildRejectsNilFatalHook(t *testing.T) {
builder := frameworkapp.NewBuilder()
if err := Register(
builder, Config{SQLitePath: filepath.Join(t.TempDir(), "officer.db")},
); err != nil {
t.Fatalf("Register: %v", err)
}
builder.SetStoreFactory(func() (store.Store, error) {
t.Error("Build opened the store despite a nil fatal shutdown hook")
return nil, errors.New("store must not be opened")
})
builder.SetNodeBuilder(fakeEngineNodeBuilder)
var nilHook frameworkapp.FatalShutdownHook
app, err := builder.Build(context.Background(), slog.Default(), nilHook)
if err == nil {
_ = app.Close()
t.Fatal("Build accepted a nil fatal shutdown hook")
}
if !strings.Contains(err.Error(), "nil fatal shutdown hook") {
t.Fatalf("Build error = %v, want the missing fatal shutdown hook named", err)
}
}
// testMasterKey returns the fixed master key of the compositions that seal
// their database.
func testMasterKey(t *testing.T) secret.MasterKey {
t.Helper()
key, err := secret.ParseMasterKey("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
if err != nil {
t.Fatalf("ParseMasterKey: %v", err)
}
return key
}
// failOnFatal returns the fatal-shutdown hook of a test that expects no
// fail-stop: any call fails the test.
func failOnFatal(t *testing.T) frameworkapp.FatalShutdownHook {
t.Helper()
return func(err error) {
t.Errorf("unexpected fatal shutdown: %v", err)
}
}
func TestRESTMutationAuditUsesResolvedPrincipal(t *testing.T) {
const principal = "alice"
app, realm, _ := buildTestApp(t, func(*http.Request) (domain.Caller, error) {
return domain.Caller{Principal: principal}, nil
}, nil)
if err := realm.CreatePrincipal(context.Background(), domain.Principal{Code: principal}); err != nil {
t.Fatalf("CreatePrincipal: %v", err)
}
handler, err := app.BuildServeHandler(nil, "/mcp")
if err != nil {
t.Fatalf("BuildServeHandler: %v", err)
}
accesstest.CompleteOpen(t, handler, accountmode.Request{Mode: accountmode.ModeMulti}, false)
req := httptest.NewRequest(
http.MethodPut,
"http://127.0.0.1/api/v1/mcp-access/get_account_state",
strings.NewReader(`{"enabled":false}`),
)
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("mutation status = %d, body = %q", rec.Code, rec.Body.String())
}
rows, err := app.Service().ListAudit(
auth.ContextWithCaller(context.Background(), auth.SystemCaller()), 100,
)
if err != nil {
t.Fatalf("ListAudit: %v", err)
}
row := auditRowForAction(t, rows, domain.AuditActionSetMcpAccess)
if row.Actor != principal || row.Source != domain.SourceAPI {
t.Fatalf("audit caller = (%q, %q), want (%q, %q)",
row.Actor, row.Source, principal, domain.SourceAPI)
}
}
func TestRegisterRESTForceUsesHostAuthorizer(t *testing.T) {
for _, wrapped := range []bool{true, false} {
t.Run(fmt.Sprintf("wrapped=%t", wrapped), func(t *testing.T) {
const principal = "alice"
const class = "equity"
refusal := fmt.Errorf("forced deletion denied: %w", domain.ErrForbidden)
forceCalls := 0
app, realm, _ := buildTestApp(t, func(*http.Request) (domain.Caller, error) {
return domain.Caller{Principal: principal}, nil
}, nil, func(builder *frameworkapp.Builder) {
if !wrapped {
return
}
installed := builder.Authorizer()
builder.SetAuthorizer(accessAuthorizer(func(
ctx context.Context, caller domain.Caller, identifier string,
) error {
if identifier == backend.ForceDeleteAssetClass {
forceCalls++
if caller.Principal != principal || caller.Source != domain.SourceAPI {
t.Errorf("force caller = %+v, want %s from API", caller, principal)
}
return refusal
}
return installed.Authorize(ctx, caller, identifier)
}))
})
ctx := context.Background()
if err := realm.CreatePrincipal(ctx, domain.Principal{Code: principal}); err != nil {
t.Fatalf("CreatePrincipal: %v", err)
}
handler, err := app.BuildServeHandler(nil, "/mcp")
if err != nil {
t.Fatalf("BuildServeHandler: %v", err)
}
accesstest.CompleteOpen(t, handler, accountmode.Request{Mode: accountmode.ModeMulti}, false)
restCall(t, handler, http.MethodPost, "/asset-classes",
`{"code":"equity"}`, http.StatusCreated)
if !wrapped {
rec := serveREST(handler, http.MethodDelete, "/asset-classes/"+class+"?force=true", "")
if rec.Code != http.StatusNoContent {
t.Fatalf("open forced delete = %d, body = %s", rec.Code, rec.Body)
}
} else {
refused := restCall(t, handler, http.MethodDelete,
"/asset-classes/"+class+"?force=true", "", http.StatusForbidden)
if errObj, _ := refused["error"].(map[string]any); errObj["code"] != "forbidden" {
t.Fatalf("error code = %v, want forbidden", errObj["code"])
}
readCtx := auth.ContextWithCaller(ctx, auth.SystemCaller())
classes, err := app.Service().ListAssetClasses(readCtx)
if err != nil || len(classes) != 1 || classes[0].Code != class {
t.Fatalf("classes after refusal = %+v, %v, want %s", classes, err, class)
}
rows, err := app.Service().ListAudit(readCtx, 100)
if err != nil {
t.Fatalf("ListAudit: %v", err)
}
deletes := 0
for _, row := range rows {
if row.Action != domain.AuditActionDeleteAssetClass {
continue
}
deletes++
if row.Detail.Code != domain.AuditDetailDeleteAssetClassForceRefused ||
row.Detail.Params["class"] != class || row.Detail.Params["refusal"] != refusal.Error() ||
row.Actor != principal || row.Source != domain.SourceAPI {
t.Fatalf("refusal audit = %+v", row)
}
}
if deletes != 1 {
t.Fatalf("delete audit rows = %d, want 1", deletes)
}
rec := serveREST(handler, http.MethodDelete, "/asset-classes/"+class, "")
if rec.Code != http.StatusNoContent || forceCalls != 1 {
t.Fatalf("unforced delete = %d, force calls = %d: %s", rec.Code, forceCalls, rec.Body)
}
}
classes, err := app.Service().ListAssetClasses(auth.ContextWithCaller(ctx, auth.SystemCaller()))
if err != nil || len(classes) != 0 {
t.Fatalf("classes after deletion = %+v, %v, want none", classes, err)
}
})
}
}
func TestRecordServiceLifecycleUsesContextCaller(t *testing.T) {
const principal = "lifecycle-admin"
app, realm, _ := buildTestApp(t, func(*http.Request) (domain.Caller, error) {
return domain.Caller{Principal: domain.PrincipalOperator}, nil
}, nil)
if err := realm.CreatePrincipal(context.Background(), domain.Principal{Code: principal}); err != nil {
t.Fatalf("CreatePrincipal: %v", err)
}
caller := domain.Caller{Source: domain.SourcePanel, Principal: principal}
if err := app.RecordServiceLifecycle(
auth.ContextWithCaller(context.Background(), caller),
domain.AuditActionRestartService,
domain.NewAuditDetail(domain.AuditDetailRestartService, nil),
); err != nil {
t.Fatalf("RecordServiceLifecycle: %v", err)
}
rows, err := app.Service().ListAudit(
auth.ContextWithCaller(context.Background(), auth.SystemCaller()), 100,
)
if err != nil {
t.Fatalf("ListAudit: %v", err)
}
row := auditRowForAction(t, rows, domain.AuditActionRestartService)
if row.Actor != principal || row.Source != domain.SourcePanel {
t.Fatalf("audit caller = (%q, %q), want (%q, %q)",
row.Actor, row.Source, principal, domain.SourcePanel)
}
}
func TestRecordServiceLifecycleRejectsUnresolvedCaller(t *testing.T) {
ctx := context.Background()
app, realm, _ := buildTestApp(t, func(*http.Request) (domain.Caller, error) {
return domain.Caller{Principal: domain.PrincipalOperator}, nil
}, nil)
before, err := realm.ListAudit(ctx, 1000)
if err != nil {
t.Fatalf("ListAudit before: %v", err)
}
err = app.RecordServiceLifecycle(
ctx, domain.AuditActionRestartService,
domain.NewAuditDetail(domain.AuditDetailRestartService, nil),
)
if !errors.Is(err, domain.ErrInvalid) || !strings.Contains(err.Error(), "no caller in context") {
t.Errorf("RecordServiceLifecycle without a caller = %v, want the missing caller named", err)
}
after, err := realm.ListAudit(ctx, 1000)
if err != nil {
t.Fatalf("ListAudit after: %v", err)
}
if len(after) != len(before) {
t.Fatalf("audit rows = %d after the rejected request, want %d: %+v",
len(after), len(before), after)
}
}
func TestRecordServiceLifecycleRecordsExplicitSystemCaller(t *testing.T) {
app, _, _ := buildTestApp(t, func(*http.Request) (domain.Caller, error) {
return domain.Caller{Principal: domain.PrincipalOperator}, nil
}, nil)
if err := app.RecordServiceLifecycle(
auth.ContextWithCaller(
context.Background(), domain.Caller{Source: domain.SourceSystem},
),
domain.AuditActionRestartService,
domain.NewAuditDetail(domain.AuditDetailRestartService, nil),
); err != nil {
t.Fatalf("RecordServiceLifecycle: %v", err)
}
rows, err := app.Service().ListAudit(
auth.ContextWithCaller(context.Background(), auth.SystemCaller()), 100,
)
if err != nil {
t.Fatalf("ListAudit: %v", err)
}
row := auditRowForAction(t, rows, domain.AuditActionRestartService)
if row.Actor != "" || row.Source != domain.SourceSystem {
t.Fatalf("audit caller = (%q, %q), want (%q, %q)",
row.Actor, row.Source, "", domain.SourceSystem)
}
}
// TestRegisterSingleAccountMode drives, over REST, the real composition started
// on a database that stores single-account mode for the account "default": a
// client never names the account and any other one is refused, lists show the
// default account alone, the account set cannot change, and a stored switch
// back to multi-account mode waits for a restart.
func TestRegisterSingleAccountMode(t *testing.T) {
// The composition generates a signing key, sealed under this master key.
key := testMasterKey(t)
_, handler := serveSeededApp(t, Config{
SQLitePath: filepath.Join(t.TempDir(), "officer.db"), MasterKey: &key,
}, map[string]string{
accountmode.SettingKeyMode: "single:default",
})
assertAccountMode(t, handler, "/app/api/v1", http.MethodGet, "",
`{"mode":"single","account":"default",`+
`"features":{"accounts":false,"account":"default"},"restartRequired":false,"live":true}`)
if got := listedAccounts(t, handler, "/app/api/v1"); !slices.Equal(got, []string{"default"}) {
t.Fatalf("listed accounts in setup = %v, want the default account alone", got)
}
accesstest.CompleteOpen(t, handler,
accountmode.Request{Mode: accountmode.ModeSingle, Account: "default"}, true)
restCall(t, handler, http.MethodPost, "/signing/keys/generate", "", http.StatusCreated)
order := `"baseAsset":"AAPL","quoteAsset":"USD","side":"buy",` +
`"amountKind":"quantity","amountValue":"1","price":"100"`
submitted := restCall(t, handler, http.MethodPost, "/orders/submit",
`{`+order+`,"mode":"hold"}`, http.StatusCreated)
id, _ := submitted["id"].(string)
detail := restCall(t, handler, http.MethodGet, "/orders/"+url.PathEscape(id), "", http.StatusOK)
if stored, _ := detail["order"].(map[string]any); stored["account"] != "default" {
t.Fatalf("submitted order = %v, want it bound to the default account", stored)
}
refused := restCall(t, handler, http.MethodPost, "/orders/check",
`{"account":"other",`+order+`}`, http.StatusUnprocessableEntity)
problems, _ := refused["errors"].([]any)
if problem, _ := firstOf(problems).(map[string]any); len(problems) != 1 ||
problem["pointer"] != "/account" || problem["constraint"] != "single_account_mode" {
t.Fatalf("check naming another account = %v, want single_account_mode at /account", refused)
}
if got := listedAccounts(t, handler, "/api/v1"); !slices.Equal(got, []string{"default"}) {
t.Fatalf("listed accounts = %v, want the default account alone", got)
}
created := restCall(t, handler, http.MethodPost, "/accounts", `{"code":"desk"}`, http.StatusConflict)
if errObj, _ := created["error"].(map[string]any); errObj["code"] != "accounts_not_managed" {
t.Fatalf("account create = %v, want accounts_not_managed", created)
}
assertAccountMode(t, handler, "/api/v1", http.MethodPut, `{"mode":"multi"}`,
`{"mode":"multi","account":"",`+
`"features":{"accounts":false,"account":"default"},"restartRequired":true,"live":false}`)
if got := listedAccounts(t, handler, "/api/v1"); !slices.Equal(got, []string{"default"}) {
t.Fatalf("listed accounts after the stored switch = %v, want the default account alone", got)
}
switched := restCall(t, handler, http.MethodPut, "/account-mode",
`{"mode":"single","account":"other"}`, http.StatusConflict)
if errObj, _ := switched["error"].(map[string]any); errObj["code"] != "accounts_not_managed" ||
!strings.Contains(fmt.Sprint(errObj["message"]), "put multi-account mode into effect first") {
t.Fatalf("switch to another default account = %v, want accounts_not_managed with the way out", switched)
}
}
// TestRegisterWithoutAccountMode starts the real composition on a database
// with no stored account mode: clients see every account, as before the mode
// existed, the mode reads as not chosen, and refused mode choices answer with
// their own codes.
func TestRegisterWithoutAccountMode(t *testing.T) {
_, handler := serveSeededApp(
t, Config{SQLitePath: filepath.Join(t.TempDir(), "officer.db")}, nil,
)
assertAccountMode(t, handler, "/app/api/v1", http.MethodGet, "",
`{"mode":null,"account":"","features":{"accounts":true,"account":""},"restartRequired":false,"live":true}`)
got := listedAccounts(t, handler, "/app/api/v1")
slices.Sort(got)
if !slices.Equal(got, []string{"default", "other"}) {
t.Fatalf("listed accounts = %v, want every account", got)
}
refused := restCall(t, handler, http.MethodPut, "/app/api/v1/account-mode", `{"mode":"solo"}`,
http.StatusUnprocessableEntity)
problems, _ := refused["errors"].([]any)
if problem, _ := firstOf(problems).(map[string]any); len(problems) != 1 ||
problem["pointer"] != "/mode" || problem["constraint"] != "enum" {
t.Fatalf("unknown mode = %v, want enum at /mode", refused)
}
missing := restCall(t, handler, http.MethodPut, "/app/api/v1/account-mode",
`{"mode":"single","account":"ghost"}`, http.StatusNotFound)
if errObj, _ := missing["error"].(map[string]any); errObj["code"] != "account_missing" ||
errObj["account"] != "ghost" {
t.Fatalf("single with a missing account = %v, want account_missing for ghost", missing)
}
}
// TestRegisterSingleAccountModeMCPSchemas lists the tools over the served MCP
// endpoint of the real composition started in single-account mode: the MCP
// registrar sees the scope Register installs, so submit_order neither requires
// an account nor offers an account group.
func TestRegisterSingleAccountModeMCPSchemas(t *testing.T) {
_, handler := serveSeededApp(t, Config{
SQLitePath: filepath.Join(t.TempDir(), "officer.db"),
}, map[string]string{
accountmode.SettingKeyMode: "single:default",
})
accesstest.CompleteOpen(t, handler,
accountmode.Request{Mode: accountmode.ModeSingle, Account: "default"}, false)
assertOneAccountSubmitSchema(t, handler)
}
// TestRegisterSingleAccountModeChosenInSetup chooses single-account mode over
// REST while the real composition, started with no mode stored, is in setup:
// the running process takes it at once, REST and MCP act on the default
// account once setup is complete, and from then on a switch waits for a
// restart.
func TestRegisterSingleAccountModeChosenInSetup(t *testing.T) {
_, handler := serveSeededApp(t, Config{
SQLitePath: filepath.Join(t.TempDir(), "officer.db"),
}, nil)
assertAccountMode(t, handler, "/app/api/v1", http.MethodPut,
`{"mode":"single","account":"default"}`,
`{"mode":"single","account":"default",`+
`"features":{"accounts":false,"account":"default"},"restartRequired":false,"live":true}`)
accesstest.CompleteOpen(t, handler,
accountmode.Request{Mode: accountmode.ModeSingle, Account: "default"}, false)
if got := listedAccounts(t, handler, "/api/v1"); !slices.Equal(got, []string{"default"}) {
t.Fatalf("listed accounts = %v, want the default account alone", got)
}
created := restCall(t, handler, http.MethodPost, "/accounts", `{"code":"desk"}`, http.StatusConflict)
if errObj, _ := created["error"].(map[string]any); errObj["code"] != "accounts_not_managed" {
t.Fatalf("account create = %v, want accounts_not_managed", created)
}
assertOneAccountSubmitSchema(t, handler)
assertAccountMode(t, handler, "/api/v1", http.MethodPut, `{"mode":"multi"}`,
`{"mode":"multi","account":"",`+
`"features":{"accounts":false,"account":"default"},"restartRequired":true,"live":false}`)
}
// TestRegisterMCPServerVersionNonRelease initializes an MCP session over the
// served /mcp endpoint of the real composition: a test binary carries no
// release tag, so serverInfo names the server and reports the non-release
// version "dev", not the engine version.
func TestRegisterMCPServerVersionNonRelease(t *testing.T) {
_, handler := serveSeededApp(t, Config{
SQLitePath: filepath.Join(t.TempDir(), "officer.db"),
}, nil)
accesstest.CompleteOpen(t, handler,
accountmode.Request{Mode: accountmode.ModeSingle, Account: "default"}, false)
server := httptest.NewServer(handler)
t.Cleanup(server.Close)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
client := sdkmcp.NewClient(&sdkmcp.Implementation{Name: "test"}, nil)
session, err := client.Connect(ctx, &sdkmcp.StreamableClientTransport{Endpoint: server.URL + "/mcp"}, nil)
if err != nil {
t.Fatalf("connect MCP: %v", err)
}
t.Cleanup(func() { _ = session.Close() })
info := session.InitializeResult().ServerInfo
if info == nil {
t.Fatal("initialize result carries no serverInfo")
}
if info.Name != "pit-officer" || info.Version != "dev" {
t.Fatalf("serverInfo = {name %q, version %q}, want {name %q, version %q}",
info.Name, info.Version, "pit-officer", "dev")
}
}
// assertOneAccountSubmitSchema lists the tools over the served MCP endpoint
// of handler: submit_order neither requires an account nor offers an account
// group.
func assertOneAccountSubmitSchema(t *testing.T, handler http.Handler) {
t.Helper()
server := httptest.NewServer(handler)
t.Cleanup(server.Close)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
client := sdkmcp.NewClient(&sdkmcp.Implementation{Name: "test"}, nil)
session, err := client.Connect(ctx, &sdkmcp.StreamableClientTransport{Endpoint: server.URL + "/mcp"}, nil)
if err != nil {
t.Fatalf("connect MCP: %v", err)
}
t.Cleanup(func() { _ = session.Close() })
list, err := session.ListTools(ctx, nil)
if err != nil {
t.Fatalf("ListTools: %v", err)
}
index := slices.IndexFunc(list.Tools, func(tool *sdkmcp.Tool) bool {
return tool.Name == "submit_order"
})
if index < 0 {
t.Fatal("submit_order missing from tools/list")
}
raw, err := json.Marshal(list.Tools[index].InputSchema)
if err != nil {
t.Fatalf("marshal submit_order input schema: %v", err)
}
var schema struct {
Required []string `json:"required"`
Properties map[string]any `json:"properties"`
}
if err := json.Unmarshal(raw, &schema); err != nil {
t.Fatalf("decode submit_order input schema: %v", err)
}
if _, group := schema.Properties["accountGroup"]; slices.Contains(schema.Required, "account") ||
group || schema.Properties["account"] == nil {
t.Fatalf("submit_order input schema = %s, want account optional and no accountGroup", raw)
}
}
// fakeSetupGate is a SetupGate whose setup state the test sets; inside reports
// whether fn is running under the gate.
type fakeSetupGate struct {
err error
inside bool
}
func (g *fakeSetupGate) WhileInSetup(fn func() error) error {
if g.err != nil {
return g.err
}
g.inside = true
defer func() { g.inside = false }()
return fn()
}
// TestSerialiseWithSetup pins the setup-time wrap of a setup decision's write
// route: during setup the handler runs inside the gate, so setup cannot
// complete between the write and its rollback; after setup it runs alone; a
// gate failure answers the error without running it; an unknown route panics.
func TestSerialiseWithSetup(t *testing.T) {
const id = "decision.put"
serve := func(t *testing.T, gate *fakeSetupGate) (ran, underGate bool, status int) {
t.Helper()
routes := &httpx.RouteRegistry{}
routes.Register(httpx.Route{
ID: id, Method: http.MethodPut, Pattern: "/decision",
Handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
ran, underGate = true, gate.inside
w.WriteHeader(http.StatusNoContent)
}),
})
serialiseWithSetup(routes, id, gate)
rec := httptest.NewRecorder()
routes.Routes()[0].Handler.ServeHTTP(rec, httptest.NewRequest(http.MethodPut, "/decision", nil))
return ran, underGate, rec.Code
}
t.Run("during setup", func(t *testing.T) {
ran, underGate, status := serve(t, &fakeSetupGate{})
if !ran || !underGate || status != http.StatusNoContent {
t.Fatalf("ran=%v underGate=%v status=%d, want the handler run under the gate, 204",
ran, underGate, status)
}
})
t.Run("after setup", func(t *testing.T) {
ran, underGate, status := serve(t, &fakeSetupGate{err: fmt.Errorf("done: %w", domain.ErrSetupComplete)})
if !ran || underGate || status != http.StatusNoContent {
t.Fatalf("ran=%v underGate=%v status=%d, want the handler run alone, 204",
ran, underGate, status)
}
})
t.Run("gate failure", func(t *testing.T) {
ran, _, status := serve(t, &fakeSetupGate{err: errors.New("access: unknown access mode")})
if ran || status != http.StatusInternalServerError {
t.Fatalf("ran=%v status=%d, want no handler run and 500", ran, status)
}
})
t.Run("unknown route", func(t *testing.T) {
defer func() {
if recover() == nil {
t.Fatal("serialiseWithSetup of an unknown route did not panic")
}
}()
serialiseWithSetup(&httpx.RouteRegistry{}, id, &fakeSetupGate{})
})
}
// TestRegisterRouteBaseline pins the route manifest of the assembled serve
// handler: the open access mode; every v1 route - the httpapi registry, the
// account-mode routes, then the lifecycle routes the serve wrapper adds - as api
// on /api/v1 and as panel on /app/api/v1, with the manifest on /api/v1 only; the
// panel-only access routes, public or panel; the agent MCP methods; the public
// mounts and SPA; and exactly the panel routes that answer during first-run
// setup.
func TestRegisterRouteBaseline(t *testing.T) {
app, _, opened := buildTestApp(t, func(*http.Request) (domain.Caller, error) {
return domain.Caller{Principal: domain.PrincipalOperator}, nil
}, nil)
sealReports, err := sealReportSource(opened)
if err != nil {
t.Fatal(err)
}
handler, err := app.BuildServeHandler(nil, "/mcp", httpapi.ServiceLifecycleRoutes(
http.NotFoundHandler(), http.NotFoundHandler(),
)...)
if err != nil {
t.Fatalf("BuildServeHandler: %v", err)
}
accesstest.CompleteOpen(t, handler, accountmode.Request{Mode: accountmode.ModeMulti}, false)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, httptest.NewRequest(
http.MethodGet, "http://127.0.0.1/api/v1/route-manifest.json", nil,
))
var manifest struct {
AccessMode string `json:"accessMode"`
Routes []struct {
Method string `json:"method"`
Path string `json:"path"`
Access string `json:"access"`
DuringSetup bool `json:"duringSetup"`
} `json:"routes"`
}
if rec.Code != http.StatusOK {
t.Fatalf("route manifest = %d: %s", rec.Code, rec.Body)
}
if err := json.Unmarshal(rec.Body.Bytes(), &manifest); err != nil {
t.Fatalf("decode route manifest: %v", err)
}
if manifest.AccessMode != "open" {
t.Fatalf("access mode = %q, want open", manifest.AccessMode)
}
var got, duringSetup []string
for _, route := range manifest.Routes {
got = append(got, route.Method+" "+route.Path+" "+route.Access)
if route.DuringSetup {
duringSetup = append(duringSetup, route.Method+" "+route.Path)
}
}
type operation struct{ method, pattern string }
var v1 []operation
for _, route := range httpapi.NewRouteRegistry(app.Service(), nil, sealReports).Routes() {
v1 = append(v1, operation{route.Method, route.Pattern})
}
v1 = append(v1,
operation{http.MethodGet, "/account-mode"},
operation{http.MethodPut, "/account-mode"},
operation{http.MethodGet, "/database/pre-migration-backups"},
operation{http.MethodPost, "/database/pre-migration-backups/{id}/download"},
operation{http.MethodDelete, "/database/pre-migration-backups/{id}"},
operation{http.MethodPost, "/service/restart"},
operation{http.MethodPost, "/service/stop"},
)
var want []string
for _, op := range v1 {
want = append(want, op.method+" /api/v1"+op.pattern+" api")
}
want = append(want, "GET /api/v1/route-manifest.json api")
for _, op := range v1 {
want = append(want, op.method+" /app/api/v1"+op.pattern+" panel")
}
want = append(want,
"GET /app/api/v1/session public",
"POST /app/api/v1/session public",
"DELETE /app/api/v1/session panel",
"DELETE /app/api/v1/sessions panel",
"POST /app/api/v1/session/recovery public",
"POST /app/api/v1/session/recovery/reset public",
"GET /app/api/v1/setup panel",
"PUT /app/api/v1/setup/progress panel",
"PUT /app/api/v1/setup/encryption panel",
"POST /app/api/v1/setup/complete panel",
"GET /app/api/v1/access panel",
"PUT /app/api/v1/access/mode panel",
"PUT /app/api/v1/access/password panel",
"GET /app/api/v1/access/tokens/{kind} panel",
"POST /app/api/v1/access/tokens/{kind}/regenerate panel",
"GET /app/api/v1/build public",
"GET /mcp agent", "POST /mcp agent", "DELETE /mcp agent",
"GET /healthz public", "GET /api/openapi.yaml public", "GET /docs public",
"GET /docs/swagger-ui.css public", "GET /docs/swagger-ui-bundle.js public",
"GET /docs/swagger-initializer.js public",
"GET /docs/swagger-ui-bundle.js.LICENSE.txt public",
"GET /docs/LICENSE public", "GET /docs/NOTICE public",
"GET /* public",
)
if !slices.Equal(got, want) {
t.Fatalf("assembled route surface mismatch\ngot: %v\nwant: %v", got, want)
}
wantDuringSetup := []string{
"GET /app/api/v1/build",
"GET /app/api/v1/credential-storage",
"GET /app/api/v1/accounts",
"POST /app/api/v1/signing/keys/generate",
"POST /app/api/v1/signing/keys/import",
"GET /app/api/v1/signing/keys",
"GET /app/api/v1/signing/keys/active/public",
"GET /app/api/v1/signing/keys/{keyId}/public",
"GET /app/api/v1/signing/config",
"PUT /app/api/v1/signing/config",
"GET /app/api/v1/account-mode",
"PUT /app/api/v1/account-mode",
"GET /app/api/v1/limits/spot-funds-limit-mode",
"PUT /app/api/v1/limits/spot-funds-limit-mode",
"POST /app/api/v1/service/restart",
"GET /app/api/v1/session",
"GET /app/api/v1/setup",
"PUT /app/api/v1/setup/progress",
"PUT /app/api/v1/setup/encryption",
"POST /app/api/v1/setup/complete",
}
slices.Sort(duringSetup)
slices.Sort(wantDuringSetup)
if !slices.Equal(duringSetup, wantDuringSetup) {
t.Fatalf("routes answering during setup mismatch\ngot: %v\nwant: %v",
duringSetup, wantDuringSetup)
}
}
func TestRegisterPreMigrationBackups(t *testing.T) {
ctx := context.Background()
path := filepath.Join(t.TempDir(), "officer.db")
builder := frameworkapp.NewBuilder()
if err := Register(builder, Config{SQLitePath: path}); err != nil {
t.Fatal(err)
}
var opened store.Store
var backupNode node.Node
builder.SetNodeBuilder(func(ctx context.Context, st store.Store, fatalHook frameworkapp.FatalShutdownHook) (node.Node, engine.Engine, error) {
opened = st
n, eng, err := fakeEngineNodeBuilder(ctx, st, fatalHook)
backupNode = n
return n, eng, err
})
builder.SetSPAFactory(func() (fs.FS, error) {
return fstest.MapFS{"index.html": {Data: []byte("<html></html>")}}, nil
})
app, err := builder.Build(ctx, slog.Default(), failOnFatal(t))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = app.Close() })
realm, err := opened.ForRealm(ctx, domain.DefaultRealm)
if err != nil {
t.Fatal(err)
}
handler, err := app.BuildServeHandler(nil, "/mcp")
if err != nil {
t.Fatal(err)
}
accesstest.CompleteOpen(t, handler, accountmode.Request{Mode: accountmode.ModeMulti}, false)
const base = "/database/pre-migration-backups"
const name = "officer.db.pre-migration-20261028T120000Z"
const olderName = "officer.db.pre-migration-20261027T120000Z"
const payload = "SQLite format 3\x00raw pre-migration bytes\xff"
copyPath := path + ".pre-migration-20261028T120000Z"
for _, copy := range []struct{ path, body string }{
{copyPath, payload},
{path + ".pre-migration-20261027T120000Z", "older"},
{filepath.Join(filepath.Dir(path), "foreign.db"), "foreign"},
} {
if err := os.WriteFile(copy.path, []byte(copy.body), 0600); err != nil {
t.Fatal(err)
}
}
readAudit := func() []domain.AuditRow {
t.Helper()
rows, err := realm.ListAudit(ctx, 1000)
if err != nil {
t.Fatal(err)
}
return rows
}
before := len(readAudit())
listed := serveREST(handler, http.MethodGet, base, "")
var list struct {
Backups []httpapi.PreMigrationBackup `json:"backups"`
}
if err := json.Unmarshal(listed.Body.Bytes(), &list); err != nil || listed.Code != http.StatusOK {
t.Fatalf("list = %d %s: %v", listed.Code, listed.Body, err)
}
if len(list.Backups) != 2 || list.Backups[0].Name != name || list.Backups[1].Name != olderName || list.Backups[0].SizeBytes != int64(len(payload)) || list.Backups[0].CreatedAt.Format(time.RFC3339) != "2026-10-28T12:00:00Z" {
t.Fatalf("listed copies = %+v", list.Backups)
}
t.Run("cross-site download", func(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "http://127.0.0.1/api/v1"+base+"/"+name+"/download", nil)
req.Header.Set("Sec-Fetch-Site", "cross-site")
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
var answer struct {
Error struct{ Code string } `json:"error"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &answer); err != nil ||
rec.Code != http.StatusForbidden || answer.Error.Code != "origin_not_allowed" {
t.Fatalf("cross-site download = %d %s, want 403 origin_not_allowed", rec.Code, rec.Body)
}
if rec.Code != http.StatusForbidden || len(readAudit()) != before {
t.Fatalf("cross-site download = %d %s, audit rows = %d, want 403 and %d rows", rec.Code, rec.Body, len(readAudit()), before)
}
})
download := serveREST(handler, http.MethodPost, base+"/"+name+"/download", "")
if download.Code != http.StatusOK || download.Body.String() != payload {
t.Fatalf("download = %d %q", download.Code, download.Body.String())
}
rows := readAudit()
if len(rows) != before+1 {
t.Fatalf("audit rows after download = %d, want %d", len(rows), before+1)
}
downloadRow := auditRowForAction(t, rows, domain.AuditActionDownloadPreMigrationBackup)
if downloadRow.Detail.Code != domain.AuditDetailDownloadPreMigrationBackup || downloadRow.Detail.Params["name"] != name || downloadRow.Actor != domain.PrincipalOperator || downloadRow.Source != domain.SourceAPI {
t.Fatalf("download audit = %+v", downloadRow)
}
refuse := func(t *testing.T, name string, status int) {
t.Helper()
before := len(readAudit())
for _, method := range []string{http.MethodPost, http.MethodDelete} {
requestPath := base + "/" + url.PathEscape(name)
if method == http.MethodPost {
requestPath += "/download"
}
rec := serveREST(handler, method, requestPath, "")
if rec.Code != status {
t.Fatalf("%s %s = %d %s, want %d", method, requestPath, rec.Code, rec.Body, status)
}
}
if after := len(readAudit()); after != before {
t.Fatalf("refused copy added audit rows: before %d, after %d", before, after)
}
}
for _, tc := range []struct {
label, name string
status int
}{
{"traversal", "../officer.db", http.StatusUnprocessableEntity},
{"absolute path", path, http.StatusUnprocessableEntity},
{"foreign file", "foreign.db", http.StatusUnprocessableEntity},
{"foreign copy", "foreign.db.pre-migration-20261028T120000Z", http.StatusUnprocessableEntity},
{"missing copy", "officer.db.pre-migration-20261026T120000Z", http.StatusNotFound},
} {
t.Run(tc.label, func(t *testing.T) { refuse(t, tc.name, tc.status) })
}
t.Run("symlink", func(t *testing.T) {
const linkName = "officer.db.pre-migration-20261029T120000Z"
link := path + ".pre-migration-20261029T120000Z"
if err := os.Symlink(path, link); err != nil {
if runtime.GOOS == "windows" && errors.Is(err, syscall.Errno(1314)) {
t.Skipf("os.Symlink requires Windows privilege: %v", err)
}
t.Fatal(err)
}
refuse(t, linkName, http.StatusNotFound)
rec := serveREST(handler, http.MethodGet, base, "")
if rec.Code != http.StatusOK || strings.Contains(rec.Body.String(), linkName) {
t.Fatalf("list includes symlink or failed: %d %s", rec.Code, rec.Body)
}
if _, err := os.Lstat(link); err != nil {
t.Fatalf("refused delete changed the symlink: %v", err)
}
})
t.Run("unresolved caller", func(t *testing.T) {
before := len(readAudit())
err := preMigrationBackupSource(path, backupNode).Audit(ctx,
domain.AuditActionDeletePreMigrationBackup,
domain.NewAuditDetail(domain.AuditDetailDeletePreMigrationBackup, domain.AuditDetailParams{"name": name}))
if err == nil || !strings.Contains(err.Error(), "no caller in context") || len(readAudit()) != before {
t.Fatalf("unresolved caller audit = %v", err)
}
})
deleted := serveREST(handler, http.MethodDelete, "/app/api/v1"+base+"/"+name, "")
if deleted.Code != http.StatusNoContent || deleted.Body.Len() != 0 {
t.Fatalf("delete = %d %s", deleted.Code, deleted.Body)
}
if _, err := os.Stat(copyPath); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("deleted copy still exists: %v", err)
}
rows = readAudit()
if len(rows) != before+2 {
t.Fatalf("audit rows after delete = %d, want %d", len(rows), before+2)
}
deleteRow := auditRowForAction(t, rows, domain.AuditActionDeletePreMigrationBackup)
if deleteRow.Detail.Code != domain.AuditDetailDeletePreMigrationBackup || deleteRow.Detail.Params["name"] != name || deleteRow.Actor != domain.PrincipalOperator || deleteRow.Source != domain.SourcePanel {
t.Fatalf("delete audit = %+v", deleteRow)
}
}
func TestRegisterPreMigrationBackupMissingInputs(t *testing.T) {
source := preMigrationBackupSource(filepath.Join(t.TempDir(), "missing", "officer.db"), nil)
if copies, err := source.List(); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("missing directory list = %+v, got %v, want os.ErrNotExist", copies, err)
}
if err := source.Audit(context.Background(), domain.AuditActionDownloadPreMigrationBackup,
domain.NewAuditDetail(domain.AuditDetailDownloadPreMigrationBackup, domain.AuditDetailParams{"name": "copy"})); err == nil || !strings.Contains(err.Error(), "nil node") {
t.Fatalf("missing node audit = %v", err)
}
}
// TestRegisterBuildStatus checks build status without a session in each mode.
func TestRegisterBuildStatus(t *testing.T) {
app, _, opened := buildTestApp(t, nil, nil)
handler, err := app.BuildServeHandler(nil, "/mcp")
if err != nil {
t.Fatal(err)
}
check := func(mode string) {
t.Helper()
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet,
"http://127.0.0.1/app/api/v1/build", nil))
var got nonrelease.Status
if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
t.Fatal(err)
}
if rec.Code != http.StatusOK || got.Phase != nonrelease.PhaseReady ||
got.Release != backend.ReleaseBuild() || got.Error != "" {
t.Fatalf("build status in %s without session = %d %s", mode, rec.Code, rec.Body)
}
}
check("setup")
accesstest.CompleteOpen(t, handler, accountmode.Request{Mode: accountmode.ModeMulti}, false)
check("open")
state, ok := opened.(access.Store)
if !ok {
t.Fatal("composed store lacks access.Store")
}
enableTokenMode(t, state, "pit_api_build-status-test")
check("token")
}
// TestRegisterAccessModeFollowsService drives the real composition, with
// Register's own caller resolver, across a switch of the stored access mode:
// the route manifest and the panel's GET /session report open mode to a
// request without credentials; once token mode is on, the same handler
// refuses that manifest request with the bearer challenge and reports token
// mode to a request with the API token, and GET /session reports token mode.
func TestRegisterAccessModeFollowsService(t *testing.T) {
app, _, opened := buildTestApp(t, nil, nil)
accessState, ok := opened.(access.Store)
if !ok {
t.Fatal("the composed store holds no access state")
}
handler, err := app.BuildServeHandler(nil, "/mcp")
if err != nil {
t.Fatalf("BuildServeHandler: %v", err)
}
accesstest.CompleteOpen(t, handler, accountmode.Request{Mode: accountmode.ModeMulti}, false)
manifestMode := func(authorization string, wantStatus int) string {
t.Helper()
req := httptest.NewRequest(
http.MethodGet, "http://127.0.0.1/api/v1/route-manifest.json", nil,
)
if authorization != "" {
req.Header.Set("Authorization", authorization)
}
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != wantStatus {
t.Fatalf("route manifest = %d, want %d", rec.Code, wantStatus)
}
if wantStatus != http.StatusOK {
return rec.Header().Get("WWW-Authenticate")
}
var manifest struct {
AccessMode string `json:"accessMode"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &manifest); err != nil {
t.Fatalf("decode route manifest: %v", err)
}
return manifest.AccessMode
}
sessionMode := func() string {
t.Helper()
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, httptest.NewRequest(
http.MethodGet, "http://127.0.0.1/app/api/v1/session", nil,
))
if rec.Code != http.StatusOK {
t.Fatalf("GET /app/api/v1/session = %d, want 200: %s", rec.Code, rec.Body)
}
var status struct {
AccessMode string `json:"accessMode"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &status); err != nil {
t.Fatalf("decode GET /app/api/v1/session: %v", err)
}
return status.AccessMode
}
if mode := manifestMode("", http.StatusOK); mode != "open" {
t.Fatalf("access mode = %q, want open", mode)
}
if mode := sessionMode(); mode != "open" {
t.Fatalf("session access mode = %q, want open", mode)
}
const apiToken = "pit_api_register-test"
enableTokenMode(t, accessState, apiToken)
if challenge := manifestMode("", http.StatusUnauthorized); challenge !=
`Bearer realm="pit-officer"` {
t.Fatalf("WWW-Authenticate = %q, want the bearer challenge", challenge)
}
if mode := manifestMode("Bearer "+apiToken, http.StatusOK); mode != "token" {
t.Fatalf("access mode = %q, want token", mode)
}
if mode := sessionMode(); mode != "token" {
t.Fatalf("session access mode = %q, want token", mode)
}
}
func TestRegisterAccessWithReplacedFactories(t *testing.T) {
replacementService := func(
_ context.Context,
n node.Node,
md backend.MarketDataRuntime,
rt backend.TradingRuntime,
signer fwsigning.Service,
_ *marketdata.Registry,
_ *frameworkmcp.ToolRegistry,
) (backend.ControlPlane, error) {
return backend.New(n, md, signer, func([]byte, domain.Order) (string, error) {
return "", errUnexpectedOrderChain
}, backend.WithTradingRuntime(rt))
}
for _, tc := range []struct {
name string
factory frameworkapp.ServiceFactory
}{
{"node builder", nil},
{"node builder and service factory", replacementService},
} {
t.Run(tc.name, func(t *testing.T) {
app, _, opened := buildTestApp(t, nil, tc.factory)
handler, err := app.BuildServeHandler(nil, "/mcp")
if err != nil {
t.Fatalf("BuildServeHandler: %v", err)
}
request := func(token string, want int) {
t.Helper()
req := httptest.NewRequest(http.MethodGet, "http://127.0.0.1/api/v1/accounts", nil)
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
apitest.ExpectStatus(t, "accounts with a replaced service factory", rec, want)
}
request("", http.StatusServiceUnavailable)
accesstest.CompleteOpen(t, handler, accountmode.Request{Mode: accountmode.ModeMulti}, false)
request("", http.StatusOK)
const token = "pit_api_replaced-service"
enableTokenMode(t, opened.(access.Store), token)
request("", http.StatusUnauthorized)
request(token, http.StatusOK)
})
}
}
func TestRegisterAccessWithReplacedStoreFactory(t *testing.T) {
builder := frameworkapp.NewBuilder()
path := filepath.Join(t.TempDir(), "officer.db")
if err := Register(builder, Config{SQLitePath: path}); err != nil {
t.Fatal(err)
}
st, err := sqlite.New(path, domain.DefaultRealm)
if err != nil {
t.Fatal(err)
}
defer func() { _ = st.Close() }()
probe := &nodeProbeStore{Store: st}
type sealReportReader interface {
SealReport(context.Context) (sqlite.SealReport, bool, error)
}
builder.SetStoreFactory(func() (store.Store, error) {
return struct {
*nodeProbeStore
access.Store
sealReportReader
}{probe, st.(access.Store), st.(sealReportReader)}, nil
})
builder.SetNodeBuilder(fakeEngineNodeBuilder)
builder.SetSPAFactory(func() (fs.FS, error) {
return fstest.MapFS{"index.html": {Data: []byte("<html></html>")}}, nil
})
app, err := builder.Build(context.Background(), slog.Default(), failOnFatal(t))
if err != nil {
t.Fatalf("Build with a capable replacement store: %v", err)
}
defer func() { _ = app.Close() }()
if probe.realmCalls == 0 {
t.Fatal("Build did not use the replacement store")
}
handler, err := app.BuildServeHandler(nil, "/mcp")
if err != nil {
t.Fatalf("BuildServeHandler: %v", err)
}
request := func(token string, want int) {
t.Helper()
req := httptest.NewRequest(http.MethodGet, "http://127.0.0.1/api/v1/accounts", nil)
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
apitest.ExpectStatus(t, "accounts with a replaced store factory", rec, want)
}
request("", http.StatusServiceUnavailable)
accesstest.CompleteOpen(t, handler, accountmode.Request{Mode: accountmode.ModeMulti}, false)
request("", http.StatusOK)
const token = "pit_api_replaced-store"
enableTokenMode(t, st.(access.Store), token)
request("", http.StatusUnauthorized)
request(token, http.StatusOK)
}
// TestRegisterReleasedAgentTokenOnMCP checks that an agent token stored with
// the prefix 0.1 and 0.2 issued, pit_mcp_, still opens /mcp now that new
// agent tokens carry pit_officer_mcp_, and that a token is matched whole: the
// same body under the new prefix or an unknown one is refused.
func TestRegisterReleasedAgentTokenOnMCP(t *testing.T) {
app, _, opened := buildTestApp(t, nil, nil)
handler, err := app.BuildServeHandler(nil, "/mcp")
if err != nil {
t.Fatalf("BuildServeHandler: %v", err)
}
accesstest.CompleteOpen(t, handler, accountmode.Request{Mode: accountmode.ModeMulti}, false)
enableTokenMode(t, opened.(access.Store), "pit_api_released-agent-token")
body, ok := strings.CutPrefix(releasedAgentToken, "pit_mcp_")
if !ok {
t.Fatal("releasedAgentToken does not carry the released prefix")
}
for _, tc := range []struct {
name, token string
want int
}{
{"released prefix", releasedAgentToken, http.StatusOK},
{"new prefix, same body", "pit_officer_mcp_" + body, http.StatusUnauthorized},
{"unknown prefix, same body", "pit_unknown_" + body, http.StatusUnauthorized},
} {
req := httptest.NewRequest(http.MethodPost, "http://127.0.0.1:8787/mcp",
strings.NewReader(mcpInitializeBody))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json, text/event-stream")
req.Header.Set("Authorization", "Bearer "+tc.token)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
apitest.ExpectStatus(t, tc.name+" agent token on /mcp", rec, tc.want)
if tc.want == http.StatusOK && (!strings.Contains(rec.Body.String(), `"result":`) ||
strings.Contains(rec.Body.String(), `"error":`)) {
t.Fatalf("%s agent token: MCP initialize returned no success result", tc.name)
}
}
}
type nodeProbeStore struct {
store.Store
realmCalls, closeCalls int
}
func (s *nodeProbeStore) ForRealm(ctx context.Context, realm domain.RealmID) (store.RealmStore, error) {
s.realmCalls++
return s.Store.ForRealm(ctx, realm)
}
func (s *nodeProbeStore) Close() error {
s.closeCalls++
return s.Store.Close()
}
func TestRegisterNodeHooksRunInOrderBeforeServices(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
builder := frameworkapp.NewBuilder()
var calls []string
var opened store.Store
var builtNode node.Node
var builtEngine *fakeEngine
builder.AddNodeHook(func(gotCtx context.Context, st store.Store, n node.Node) error {
if gotCtx != ctx || st != opened || n != builtNode {
t.Fatal("node hook did not receive Build's context, store and node")
}
calls = append(calls, "first")
return nil
})
if err := Register(builder, Config{SQLitePath: filepath.Join(t.TempDir(), "officer.db")}); err != nil {
t.Fatal(err)
}
builder.SetNodeBuilder(func(ctx context.Context, st store.Store,
hook frameworkapp.FatalShutdownHook) (node.Node, engine.Engine, error) {
n, eng, err := fakeEngineNodeBuilder(ctx, st, hook)
if err == nil {
opened, builtNode, builtEngine = st, n, eng.(*fakeEngine)
}
return n, eng, err
})
wantErr := errors.New("host node hook failed")
builder.AddNodeHook(func(context.Context, store.Store, node.Node) error {
calls = append(calls, "second")
if err := builder.Authorizer().Authorize(ctx, domain.Caller{Source: domain.SourceAPI}, "test"); !errors.Is(err, domain.ErrSetupRequired) {
t.Fatalf("access hook did not run between host hooks: %v", err)
}
return wantErr
})
builder.AddNodeHook(func(context.Context, store.Store, node.Node) error {
t.Fatal("node hook ran after a hook failed")
return nil
})
builder.SetSigningFactory(func(store.RealmStore) (fwsigning.Service, error) {
t.Fatal("signing service built before node hooks completed")
return nil, nil
})
app, err := builder.Build(ctx, slog.Default(), failOnFatal(t))
if app != nil {
_ = app.Close()
t.Fatal("Build returned an app after a node hook failed")
}
if !errors.Is(err, wantErr) {
t.Fatalf("Build error = %v, want hook error %v", err, wantErr)
}
if !slices.Equal(calls, []string{"first", "second"}) {
t.Fatalf("node hook order = %v, want [first second]", calls)
}
if builtEngine == nil || builtEngine.running ||
!slices.Equal(builtEngine.lifecycle, []string{"stop", "close market data"}) {
t.Fatalf("failed node hook left engine open: %+v", builtEngine)
}
}
func TestRegisterRejectsStoreAndClosesNode(t *testing.T) {
for _, withAccess := range []bool{false, true} {
t.Run(fmt.Sprintf("access=%t", withAccess), func(t *testing.T) {
builder := frameworkapp.NewBuilder()
path := filepath.Join(t.TempDir(), "officer.db")
if err := Register(builder, Config{SQLitePath: path}); err != nil {
t.Fatal(err)
}
st, err := sqlite.New(path, domain.DefaultRealm)
if err != nil {
t.Fatal(err)
}
probe := &nodeProbeStore{Store: st}
builder.SetStoreFactory(func() (store.Store, error) {
if withAccess {
return struct {
*nodeProbeStore
access.Store
}{probe, st.(access.Store)}, nil
}
return probe, nil
})
var builtEngine *fakeEngine
var realmCallsAfterNode int
builder.SetNodeBuilder(func(ctx context.Context, st store.Store,
hook frameworkapp.FatalShutdownHook) (node.Node, engine.Engine, error) {
n, eng, err := fakeEngineNodeBuilder(ctx, st, hook)
if err == nil {
builtEngine = eng.(*fakeEngine)
realmCallsAfterNode = probe.realmCalls
}
return n, eng, err
})
app, err := builder.Build(context.Background(), slog.Default(), failOnFatal(t))
if err == nil {
_ = app.Close()
t.Fatal("Build accepted a store without Officer's access capabilities")
}
want := "does not implement access.Store"
if withAccess {
want = "does not implement SealReport"
}
if !strings.Contains(err.Error(), want) {
t.Fatalf("Build = %v, want %q", err, want)
}
if builtEngine == nil || builtEngine.running ||
!slices.Equal(builtEngine.lifecycle, []string{"stop", "close market data"}) ||
probe.realmCalls != realmCallsAfterNode || probe.closeCalls != 1 {
t.Fatalf("refused store: engine=%+v, realm calls=%d (after node=%d), close calls=%d",
builtEngine, probe.realmCalls, realmCallsAfterNode, probe.closeCalls)
}
})
}
}
// releasedAgentToken is an agent token as 0.1 and 0.2 issued it: the prefix
// pit_mcp_ and 32 random bytes in unpadded base64url.
const releasedAgentToken = "pit_mcp_Q2x7bWv9R1t4YzK8pN3sJd6HfL0aEu5cXg2iTo7Vr4w"
// mcpInitializeBody is an MCP initialize request, the first call an agent
// makes on /mcp.
const mcpInitializeBody = `{"jsonrpc":"2.0","id":1,"method":"initialize","params":{` +
`"protocolVersion":"2025-03-26","capabilities":{},` +
`"clientInfo":{"name":"register-test","version":"1"}}}`
// enableTokenMode switches the stored access mode to token mode with apiToken
// as the API token and releasedAgentToken as the agent token, past the access
// Service, which no app exposes.
func enableTokenMode(t *testing.T, state access.Store, apiToken string) {
t.Helper()
now := time.Now()
if err := state.EnableTokenMode(context.Background(), access.Credentials{
Password: access.Password{Hash: "$argon2id$test", SetAt: now},
APIToken: apiToken,
AgentToken: releasedAgentToken,
}, access.Session{
IDHash: sha256.Sum256([]byte("register-test")),
CreatedAt: now,
LastSeenAt: now,
ExpiresAt: now.Add(time.Hour),
}); err != nil {
t.Fatalf("EnableTokenMode: %v", err)
}
}
// fixedModeAccessStore publishes a chosen mode and implements Snapshot only.
type fixedModeAccessStore struct {
access.Store
mode access.Mode
}
func (s fixedModeAccessStore) Snapshot() access.Snapshot {
return access.Snapshot{Mode: s.mode}
}
// TestManifestAccessModeFailsClosed checks that the route manifest gets the
// empty access mode, which it refuses as an internal error, never open mode:
// without an access Service and for a mode it does not know.
func TestManifestAccessModeFailsClosed(t *testing.T) {
service, err := access.New(
fixedModeAccessStore{mode: "unknown"}, struct{ access.Auditor }{}, io.Discard,
struct{ access.SetupDecisions }{},
)
if err != nil {
t.Fatalf("access.New: %v", err)
}
if mode := manifestAccessMode(service); mode != "" {
t.Fatalf("manifestAccessMode(unknown mode) = %q, want the empty mode", mode)
}
}
// TestRegisterBrowserGuards drives the real composition the way a page open in
// the operator's browser can: a cross-site or text/plain write and a write
// under a re-bound host are refused before any handler runs and change
// nothing, while the panel's own same-origin JSON writes still work on both
// mounts.
func TestRegisterBrowserGuards(t *testing.T) {
app, handler := serveSeededApp(
t, Config{SQLitePath: filepath.Join(t.TempDir(), "officer.db")}, nil,
)
accesstest.CompleteOpen(t, handler, accountmode.Request{Mode: accountmode.ModeMulti}, false)
if mcpCommandEnabled(t, app, "create_account") {
t.Fatal("create_account is enabled before the hostile writes")
}
for _, tc := range []struct {
name, method, target, contentType, body string
header map[string]string
status int
code string
}{
{
name: "cross-site database reset", method: http.MethodPost,
target: "http://127.0.0.1/api/v1/database/reset",
contentType: "text/plain", body: `{"confirm":true}`,
header: map[string]string{"Origin": "http://attacker.example"},
status: http.StatusForbidden, code: "origin_not_allowed",
},
{
name: "cross-site MCP access change", method: http.MethodPut,
target: "http://127.0.0.1/api/v1/mcp-access/create_account",
contentType: "text/plain", body: `{"enabled":true}`,
header: map[string]string{"Origin": "http://attacker.example"},
status: http.StatusForbidden, code: "origin_not_allowed",
},
{
name: "text/plain database reset", method: http.MethodPost,
target: "http://127.0.0.1/api/v1/database/reset",
contentType: "text/plain", body: `{"confirm":true}`,
status: http.StatusUnsupportedMediaType, code: "unsupported_media_type",
},
{
// A page on a domain re-bound to loopback is same-origin with
// itself, so only the Host guard tells it apart.
name: "re-bound host database reset", method: http.MethodPost,
target: "http://attacker.example/api/v1/database/reset",
contentType: "application/json", body: `{"confirm":true}`,
header: map[string]string{
"Origin": "http://attacker.example", "Sec-Fetch-Site": "same-origin",
},
status: http.StatusForbidden, code: "host_not_allowed",
},
{
name: "foreign host health check", method: http.MethodGet,
target: "http://officer.example/healthz",
status: http.StatusForbidden, code: "host_not_allowed",
},
} {
req := httptest.NewRequest(tc.method, tc.target, strings.NewReader(tc.body))
req.Header.Set("Content-Type", tc.contentType)
for name, value := range tc.header {
req.Header.Set(name, value)
}
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
var answer struct {
Error struct{ Code string } `json:"error"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &answer); err != nil ||
rec.Code != tc.status || answer.Error.Code != tc.code {
t.Fatalf("%s = %d %s, want %d %s", tc.name, rec.Code, rec.Body, tc.status, tc.code)
}
}
got := listedAccounts(t, handler, "/api/v1")
slices.Sort(got)
if !slices.Equal(got, []string{"default", "other"}) {
t.Fatalf("listed accounts after the refused writes = %v, want default and other", got)
}
if mcpCommandEnabled(t, app, "create_account") {
t.Fatal("a refused write enabled create_account")
}
// Officer answers under its loopback names, on any port.
for _, host := range []string{"localhost:8787", "[::1]:8787", "127.0.0.1:9000"} {
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, httptest.NewRequest(
http.MethodGet, "http://"+host+"/healthz", nil,
))
if rec.Code != http.StatusOK {
t.Fatalf("GET /healthz under %s = %d %s, want 200", host, rec.Code, rec.Body)
}
}
// The panel's own writes: same origin, JSON bodies, on both mounts.
for _, tc := range []struct {
prefix string
enabled bool
}{
{"/app/api/v1", false},
{"/api/v1", true},
} {
req := httptest.NewRequest(
http.MethodPut,
"http://127.0.0.1:8787"+tc.prefix+"/mcp-access/get_account_state",
strings.NewReader(fmt.Sprintf(`{"enabled":%t}`, tc.enabled)),
)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Origin", "http://127.0.0.1:8787")
req.Header.Set("Sec-Fetch-Site", "same-origin")
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("same-origin JSON write on %s = %d %s, want 200", tc.prefix, rec.Code, rec.Body)
}
if got := mcpCommandEnabled(t, app, "get_account_state"); got != tc.enabled {
t.Fatalf("get_account_state enabled after the write on %s = %v, want %v",
tc.prefix, got, tc.enabled)
}
}
}
// TestRegisterAllowedHosts pins that the operator's host names add to the
// loopback names rather than replace them, and that an invalid one fails
// Register.
func TestRegisterAllowedHosts(t *testing.T) {
_, handler := serveSeededApp(t, Config{
SQLitePath: filepath.Join(t.TempDir(), "officer.db"),
AllowedHosts: []string{"officer"},
}, nil)
for _, host := range []string{"officer:8787", "OFFICER", "127.0.0.1:8787", "localhost"} {
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, httptest.NewRequest(
http.MethodGet, "http://"+host+"/healthz", nil,
))
if rec.Code != http.StatusOK {
t.Fatalf("GET /healthz under %s = %d %s, want 200", host, rec.Code, rec.Body)
}
}
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, httptest.NewRequest(
http.MethodGet, "http://attacker.example/healthz", nil,
))
var answer struct {
Error struct{ Code string } `json:"error"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &answer); err != nil ||
rec.Code != http.StatusForbidden || answer.Error.Code != "host_not_allowed" {
t.Fatalf("GET /healthz under attacker.example = %d %s, want 403 host_not_allowed",
rec.Code, rec.Body)
}
if err := Register(frameworkapp.NewBuilder(), Config{
AllowedHosts: []string{"officer:8787"},
}); err == nil {
t.Fatal("Register accepted the allowed host officer:8787")
}
}
// mcpCommandEnabled reports whether the composed MCP catalogue of app enables
// the command name.
func mcpCommandEnabled(t *testing.T, app *frameworkapp.App, name string) bool {
t.Helper()
commands, err := app.Service().ListMcpAccess(
auth.ContextWithCaller(context.Background(), auth.SystemCaller()),
)
if err != nil {
t.Fatalf("ListMcpAccess: %v", err)
}
command, ok := findCommand(commands, name)
if !ok {
t.Fatalf("%s missing from the composed MCP catalogue", name)
}
return command.Enabled
}
// serveSeededApp builds the real composition - Register over cfg, with
// Register's own node over the native engine - on a database seeded, sealed
// with cfg's master key when it carries one, with the accounts "default" and
// "other", the assets AAPL and USD, and settings, and returns the app and its
// served handler.
func serveSeededApp(
t *testing.T, cfg Config, settings map[string]string,
) (*frameworkapp.App, http.Handler) {
t.Helper()
ctx := context.Background()
var opts []sqlite.Option
if cfg.MasterKey != nil {
opts = append(opts, sqlite.WithMasterKey(*cfg.MasterKey))
}
st, err := sqlite.New(cfg.SQLitePath, domain.DefaultRealm, opts...)
if err != nil {
t.Fatalf("sqlite.New: %v", err)
}
if err := st.Migrate(ctx); err != nil {
_ = st.Close()
t.Fatalf("Migrate: %v", err)
}
realm, err := st.ForRealm(ctx, domain.DefaultRealm)
if err != nil {
_ = st.Close()
t.Fatalf("ForRealm: %v", err)
}
for _, code := range []domain.AccountID{"default", "other"} {
if _, err := realm.CreateAccount(ctx, domain.Account{Code: code}); err != nil {
t.Fatalf("CreateAccount(%s): %v", code, err)
}
}
for _, code := range []string{"AAPL", "USD"} {
if _, err := realm.CreateAsset(ctx, domain.Asset{Code: code}); err != nil {
t.Fatalf("CreateAsset(%s): %v", code, err)
}
}
for key, value := range settings {
if err := realm.SetSetting(ctx, key, value); err != nil {
t.Fatalf("SetSetting(%s): %v", key, err)
}
}
if err := st.Close(); err != nil {
t.Fatalf("close seeded store: %v", err)
}
builder := frameworkapp.NewBuilder()
if err := Register(builder, cfg); err != nil {
t.Fatalf("Register: %v", err)
}
builder.SetSPAFactory(func() (fs.FS, error) {
return fstest.MapFS{"index.html": {Data: []byte("<html></html>")}}, nil
})
app, err := builder.Build(ctx, slog.Default(), failOnFatal(t))
if err != nil {
t.Fatalf("Build: %v", err)
}
t.Cleanup(func() { _ = app.Close() })
handler, err := app.BuildServeHandler(nil, "/mcp")
if err != nil {
t.Fatalf("BuildServeHandler: %v", err)
}
return app, handler
}
// restCall serves a request through handler and decodes its JSON answer,
// failing unless the status is want. A path starting with /app/api/v1/ is
// served as given; any other path gets the /api/v1 prefix.
func restCall(
t *testing.T, handler http.Handler, method, path, body string, want int,
) map[string]any {
t.Helper()
rec := serveREST(handler, method, path, body)
if rec.Code != want {
t.Fatalf("%s %s = %d, want %d: %s", method, path, rec.Code, want, rec.Body)
}
var out map[string]any
if err := json.Unmarshal(rec.Body.Bytes(), &out); err != nil {
t.Fatalf("%s %s: decode %q: %v", method, path, rec.Body, err)
}
return out
}
// assertAccountMode serves one /account-mode request and checks its exact wire
// answer.
func assertAccountMode(t *testing.T, handler http.Handler, prefix, method, body, want string) {
t.Helper()
path := strings.TrimPrefix(prefix, "/api/v1") + "/account-mode"
rec := serveREST(handler, method, path, body)
if got := strings.TrimSpace(rec.Body.String()); rec.Code != http.StatusOK || got != want {
t.Fatalf("%s /account-mode = %d %s, want 200 %s", method, rec.Code, got, want)
}
}
// serveREST serves a path starting with /app/api/v1/ as given; any other path
// gets the /api/v1 prefix.
func serveREST(handler http.Handler, method, path, body string) *httptest.ResponseRecorder {
if !strings.HasPrefix(path, "/app/api/v1/") {
path = "/api/v1" + path
}
return serveExactPath(handler, method, path, body)
}
func serveExactPath(handler http.Handler, method, path, body string) *httptest.ResponseRecorder {
req := httptest.NewRequest(method, "http://127.0.0.1"+path, strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
return rec
}
// listedAccounts returns the codes GET /accounts lists.
func listedAccounts(t *testing.T, handler http.Handler, prefix string) []string {
t.Helper()
path := strings.TrimPrefix(prefix, "/api/v1") + "/accounts"
listed, _ := restCall(t, handler, http.MethodGet, path, "", http.StatusOK)["accounts"].([]any)
codes := make([]string, 0, len(listed))
for _, account := range listed {
row, _ := account.(map[string]any)
codes = append(codes, fmt.Sprint(row["code"]))
}
return codes
}
// firstOf returns the first element, or nil for none.
func firstOf(items []any) any {
if len(items) == 0 {
return nil
}
return items[0]
}
// buildTestApp builds Register's composition, over a fake engine, on a fresh
// database and returns the app, its realm and the store Register opened.
// resolver replaces Register's caller resolver and serviceFactory its service
// factory; nil keeps Register's. configure runs on the builder last, after
// Register and the replacements, before Build.
func buildTestApp(
t *testing.T,
resolver func(*http.Request) (domain.Caller, error),
serviceFactory frameworkapp.ServiceFactory,
configure ...func(*frameworkapp.Builder),
) (*frameworkapp.App, store.RealmStore, store.Store) {
t.Helper()
ctx := context.Background()
builder := frameworkapp.NewBuilder()
path := filepath.Join(t.TempDir(), "officer.db")
if err := Register(
builder, Config{SQLitePath: path},
); err != nil {
t.Fatalf("Register: %v", err)
}
var opened store.Store
builder.SetNodeBuilder(func(
buildCtx context.Context, st store.Store, fatalHook frameworkapp.FatalShutdownHook,
) (node.Node, engine.Engine, error) {
opened = st
return fakeEngineNodeBuilder(buildCtx, st, fatalHook)
})
if resolver != nil {
builder.SetCallerResolver(resolver)
}
if serviceFactory != nil {
builder.SetServiceFactory(serviceFactory)
}
builder.SetSPAFactory(func() (fs.FS, error) {
return fstest.MapFS{"index.html": {Data: []byte("<html></html>")}}, nil
})
for _, configureBuilder := range configure {
configureBuilder(builder)
}
app, err := builder.Build(ctx, slog.Default(), failOnFatal(t))
if err != nil {
t.Fatalf("Build: %v", err)
}
t.Cleanup(func() { _ = app.Close() })
realm, err := opened.ForRealm(ctx, domain.DefaultRealm)
if err != nil {
t.Fatalf("ForRealm: %v", err)
}
return app, realm, opened
}
func auditRowForAction(
t *testing.T, rows []domain.AuditRow, action domain.AuditAction,
) domain.AuditRow {
t.Helper()
for _, row := range rows {
if row.Action == action {
return row
}
}
t.Fatalf("audit action %q not found in %+v", action, rows)
return domain.AuditRow{}
}
func TestBuildStopsEngineReturnedWithoutNode(t *testing.T) {
builder := frameworkapp.NewBuilder()
if err := Register(
builder, Config{SQLitePath: filepath.Join(t.TempDir(), "officer.db")},
); err != nil {
t.Fatalf("Register: %v", err)
}
eng := &fakeEngine{running: true, sink: &fakeSink{}}
builder.SetNodeBuilder(func(
context.Context, store.Store, frameworkapp.FatalShutdownHook,
) (node.Node, engine.Engine, error) {
return nil, eng, nil
})
app, err := builder.Build(context.Background(), slog.Default(), failOnFatal(t))
if err == nil {
_ = app.Close()
t.Fatal("Build accepted a node hook that returned no node")
}
if !strings.Contains(err.Error(), "no node") {
t.Fatalf("Build error = %v, want the missing node named", err)
}
if eng.running {
t.Fatal("engine returned with a nil node was not stopped")
}
if len(eng.lifecycle) != 2 ||
eng.lifecycle[0] != "stop" || eng.lifecycle[1] != "close market data" {
t.Fatalf("engine shutdown calls = %v, want stop then close market data", eng.lifecycle)
}
}
// fakeEngineNodeBuilder replaces the composition's node hook with the same local
// node over a fake engine, so these tests need no native runtime.
func fakeEngineNodeBuilder(
ctx context.Context, st store.Store, fatalHook frameworkapp.FatalShutdownHook,
) (node.Node, engine.Engine, error) {
return node.NewLocalNode(
ctx,
domain.DefaultRealm,
st,
func(engine.Snapshot) (engine.Engine, error) {
return &fakeEngine{running: true, sink: &fakeSink{}}, nil
},
fatalHook,
)
}
func findCommand(commands []backend.McpCommand, name string) (backend.McpCommand, bool) {
for _, cmd := range commands {
if cmd.Command.Name == name {
return cmd, true
}
}
return backend.McpCommand{}, false
}
type fakeSink struct{}
func (s *fakeSink) Push(marketdata.QuoteUpdate) error { return nil }
type fakeEngine struct {
sink marketdata.Sink
running bool
lifecycle []string
}
var errUnexpectedOrderChain = errors.New("unexpected order chain call")
func TestManifestAccessModeSetup(t *testing.T) {
service, err := access.New(
fixedModeAccessStore{mode: access.ModeSetup}, struct{ access.Auditor }{},
io.Discard, struct{ access.SetupDecisions }{},
)
if err != nil {
t.Fatal(err)
}
if got := manifestAccessMode(service); got != httpx.AccessModeSetup {
t.Fatalf("manifestAccessMode(setup) = %q, want setup", got)
}
}
func TestRegisterFirstRun(t *testing.T) {
for _, mode := range []string{"open", "token"} {
t.Run(mode, func(t *testing.T) {
app, _, _ := buildTestApp(t, nil, nil)
handler, err := app.BuildServeHandler(nil, "/mcp")
if err != nil {
t.Fatalf("BuildServeHandler: %v", err)
}
var cookie *http.Cookie
call := func(method, path, body string, want int) *httptest.ResponseRecorder {
t.Helper()
req := httptest.NewRequest(method, "http://127.0.0.1:8787"+path,
strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
if cookie != nil {
req.AddCookie(cookie)
}
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
apitest.ExpectStatus(t, method+" "+path, rec, want)
return rec
}
assertSetup := func() {
t.Helper()
rec := call(http.MethodGet, "/app/api/v1/session", "", http.StatusOK)
const want = `{"accessMode":"setup","setupComplete":false,"authenticated":false}`
if strings.TrimSpace(rec.Body.String()) != want {
t.Fatalf("session before setup = %s, want %s", rec.Body, want)
}
for _, path := range []string{
"/api/v1/accounts", "/api/v1/route-manifest.json", "/mcp", "/app/api/v1/mcp-access",
} {
rec := call(http.MethodGet, path, "", http.StatusServiceUnavailable)
var answer struct {
Error struct{ Code, Message string } `json:"error"`
}
const message = "Pit Officer is not set up yet. Open http://127.0.0.1:8787" +
" in a browser on this computer and finish the first-run setup."
if err := json.Unmarshal(rec.Body.Bytes(), &answer); err != nil ||
answer.Error.Code != "setup_required" || answer.Error.Message != message {
t.Fatalf("GET %s = %s, want setup_required with the addressed sentence", path, rec.Body)
}
}
}
assertSetup()
rec := call(http.MethodGet, "/app/api/v1/setup", "", http.StatusOK)
if strings.TrimSpace(rec.Body.String()) != `{"step":"welcome","encryptionDeclinedAt":null}` {
t.Fatalf("fresh setup state = %s, want welcome with no encryption decision", rec.Body)
}
call(http.MethodPut, "/app/api/v1/setup/progress", `{"step":"accounts"}`, http.StatusNoContent)
assertNoFundsLimitMode := func() {
t.Helper()
rec := call(http.MethodGet, "/app/api/v1/limits/spot-funds-limit-mode", "", http.StatusOK)
const want = `{"mode":"enforce","stored":false,"accountsWithoutCurrency":[]}`
if strings.TrimSpace(rec.Body.String()) != want {
t.Fatalf("funds limit mode during setup = %s, want %s", rec.Body, want)
}
}
assertNoFundsLimitMode()
rec = call(http.MethodPost, "/app/api/v1/setup/complete",
`{"access":"open","acknowledged":true}`, http.StatusConflict)
for _, name := range []string{"account mode", "funds limit mode", "encryption"} {
if !strings.Contains(rec.Body.String(), name) {
t.Fatalf("completion without decisions = %s, want missing %s", rec.Body, name)
}
}
accounts := accountmode.Request{Mode: accountmode.ModeMulti}
if mode == "open" {
accesstest.CompleteOpen(t, handler, accounts, false)
} else {
cookie = accesstest.CompleteToken(t, handler, accounts, "first run test password")
if !cookie.HttpOnly || cookie.SameSite != http.SameSiteStrictMode || cookie.Path != "/" {
t.Fatal("setup completion issued a session cookie without the panel protections")
}
for kind, prefix := range map[string]string{"api": "pit_api_", "agent": "pit_officer_mcp_"} {
rec := call(http.MethodGet, "/app/api/v1/access/tokens/"+kind, "", http.StatusOK)
var answer struct{ Token string }
if err := json.Unmarshal(rec.Body.Bytes(), &answer); err != nil ||
!strings.HasPrefix(answer.Token, prefix) || answer.Token == prefix {
t.Fatalf("setup completion issued no %s token", kind)
}
method, path, body := http.MethodGet, "/api/v1/accounts", ""
if kind == "agent" {
method, path, body = http.MethodPost, "/mcp", mcpInitializeBody
}
req := httptest.NewRequest(method, "http://127.0.0.1:8787"+path, strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json, text/event-stream")
req.Header.Set("Authorization", "Bearer "+answer.Token)
authRec := httptest.NewRecorder()
handler.ServeHTTP(authRec, req)
apitest.ExpectStatus(t, kind+" token "+method+" "+path, authRec, http.StatusOK)
if kind == "agent" && (!strings.Contains(authRec.Body.String(), `"result":`) ||
strings.Contains(authRec.Body.String(), `"error":`)) {
t.Fatal("agent token MCP initialize returned no success result")
}
}
}
rec = call(http.MethodGet, "/app/api/v1/session", "", http.StatusOK)
want := fmt.Sprintf(`{"accessMode":%q,"setupComplete":true,"authenticated":%t}`, mode, mode == "token")
if strings.TrimSpace(rec.Body.String()) != want {
t.Fatalf("session after setup = %s, want %s", rec.Body, want)
}
if mode == "open" {
call(http.MethodGet, "/api/v1/accounts", "", http.StatusOK)
} else {
call(http.MethodGet, "/api/v1/accounts", "", http.StatusUnauthorized)
}
for _, route := range []struct{ method, path, body string }{
{http.MethodGet, "/setup", ""},
{http.MethodPut, "/setup/progress", `{"step":"access"}`},
{http.MethodPut, "/setup/encryption", `{"encryption":"none","acknowledged":true}`},
{http.MethodPost, "/setup/complete", `{"access":"open","acknowledged":true}`},
} {
rec := call(route.method, "/app/api/v1"+route.path, route.body, http.StatusConflict)
if code, _ := apitest.Refusal(rec); code != "setup_complete" {
t.Fatalf("%s %s after completion = %s, want setup_complete", route.method, route.path, rec.Body)
}
}
call(http.MethodPost, "/app/api/v1/database/reset", `{"confirm":true}`, http.StatusOK)
assertSetup()
rec = call(http.MethodGet, "/app/api/v1/setup", "", http.StatusOK)
if strings.TrimSpace(rec.Body.String()) != `{"step":"welcome","encryptionDeclinedAt":null}` {
t.Fatalf("setup after reset = %s, want welcome with no encryption decision", rec.Body)
}
assertNoFundsLimitMode()
})
}
}
func TestRegisterSetupAccessAuthorizer(t *testing.T) {
app, _, _ := buildTestApp(t, func(*http.Request) (domain.Caller, error) {
return domain.Caller{Principal: domain.PrincipalOperator}, nil
}, nil)
handler, err := app.BuildServeHandler(nil, "/mcp")
if err != nil {
t.Fatal(err)
}
for _, tc := range []struct {
path string
status int
code string
}{
{"/app/api/v1/setup", 200, ""},
{"/app/api/v1/mcp-access", 503, "setup_required"},
{"/api/v1/accounts", 503, "setup_required"},
} {
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, httptest.NewRequest("GET", "http://127.0.0.1"+tc.path, nil))
var answer struct {
Step string `json:"step"`
Error struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
err := json.Unmarshal(rec.Body.Bytes(), &answer)
if rec.Code != tc.status || err != nil || answer.Error.Code != tc.code {
t.Fatalf("GET %s = %d, error code %q, want %d %s",
tc.path, rec.Code, answer.Error.Code, tc.status, tc.code)
}
if tc.status == 200 && answer.Step != "welcome" {
t.Fatalf("GET /setup step = %q, want welcome", answer.Step)
}
if tc.code == "setup_required" && answer.Error.Message != domain.ErrSetupRequired.Error() {
t.Fatalf("GET %s message = %q, want bare setup_required %q",
tc.path, answer.Error.Message, domain.ErrSetupRequired.Error())
}
}
chosen, err := accountmode.Chosen(context.Background(), app.Service())
if err != nil || chosen {
t.Fatalf("unchosen account mode = %t, %v, want false", chosen, err)
}
rec := httptest.NewRecorder()
req := httptest.NewRequest("PUT", "http://127.0.0.1/app/api/v1/account-mode",
strings.NewReader(`{"mode":"multi"}`))
req.Header.Set("Content-Type", "application/json")
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("choose account mode = %d %s", rec.Code, rec.Body)
}
chosen, err = accountmode.Chosen(context.Background(), app.Service())
if err != nil || !chosen {
t.Fatalf("saved account mode = %t, %v, want true", chosen, err)
}
}
func (*fakeEngine) AsyncEngine() *asyncengine.AsyncEngine {
panic(errUnexpectedOrderChain)
}
func (*fakeEngine) AccountID(domain.AccountID) (param.AccountID, error) {
return param.AccountID{}, errUnexpectedOrderChain
}
func (*fakeEngine) ExecutionReportModel(
domain.ExecutionReportInput, string,
) (model.ExecutionReport, error) {
return model.ExecutionReport{}, errUnexpectedOrderChain
}
func (*fakeEngine) SettledExecutionReport(
domain.ExecutionReportInput,
param.AccountID,
pretrade.PostTradeResult,
) (engine.ExecutionReportResult, error) {
return engine.ExecutionReportResult{}, errUnexpectedOrderChain
}
func (*fakeEngine) AccountAdjustmentModels(
[]domain.AdjustmentRequest,
) ([]model.AccountAdjustment, error) {
return nil, errUnexpectedOrderChain
}
func (*fakeEngine) AppliedAccountAdjustmentBatch(
domain.AccountID,
[]domain.AdjustmentRequest,
accountadjustment.BatchResult,
) ([]engine.AdjustmentResult, *domain.AdjustmentOutcomeRejected, error) {
return nil, nil, errUnexpectedOrderChain
}
func (*fakeEngine) SpotFundsAccountPnlAssignment(
string,
domain.PnlHaltReason,
) (asyncengine.SpotFundsAccountPnlAssignment, error) {
return asyncengine.SpotFundsAccountPnlAssignment{}, errUnexpectedOrderChain
}
func (*fakeEngine) AppliedSpotFundsAccountPnl(
domain.AccountID,
string,
domain.PnlHaltReason,
configure.PolicyConfigurationResult,
) ([]domain.AccountBlock, error) {
return nil, errUnexpectedOrderChain
}
func (*fakeEngine) OrderModel(domain.Order) (model.Order, error) {
return model.Order{}, errUnexpectedOrderChain
}
func (*fakeEngine) CheckOrderModel(domain.OrderProbe) (model.Order, error) {
return model.Order{}, errUnexpectedOrderChain
}
func (*fakeEngine) RejectedOrder(
domain.Order, []reject.Reject,
) (engine.OrderResult, error) {
panic(errUnexpectedOrderChain)
}
func (*fakeEngine) ReservedOrder(
domain.Order, asyncengine.OperationResult,
) (engine.OrderResult, error) {
return engine.OrderResult{}, errUnexpectedOrderChain
}
func (*fakeEngine) AppliedDropCopyOrder(
domain.Order, asyncengine.DropCopyResult,
) (engine.OrderResult, error) {
return engine.OrderResult{}, errUnexpectedOrderChain
}
func (*fakeEngine) RejectedImmediate(
domain.Order, []reject.Reject,
) (engine.ImmediateResult, error) {
panic(errUnexpectedOrderChain)
}
func (*fakeEngine) PrepareImmediateReservation(
domain.Order, asyncengine.OperationResult,
) (engine.ImmediatePreparation, error) {
return engine.ImmediatePreparation{}, errUnexpectedOrderChain
}
func (*fakeEngine) PrepareImmediateDropCopy(
domain.Order, asyncengine.DropCopyResult,
) (engine.ImmediatePreparation, error) {
return engine.ImmediatePreparation{}, errUnexpectedOrderChain
}
func (*fakeEngine) SettleImmediate(
domain.Order, engine.ImmediatePreparation, pretrade.PostTradeResult,
) (engine.ImmediateResult, error) {
return engine.ImmediateResult{}, errUnexpectedOrderChain
}
func (*fakeEngine) CheckedOrder(
domain.OrderProbe, asyncengine.OrderCheckResult,
) (domain.CheckResult, error) {
return domain.CheckResult{}, errUnexpectedOrderChain
}
func (e *fakeEngine) Version() string { return "fake" }
func (e *fakeEngine) BuildProfile() string { return "test" }
func (e *fakeEngine) Running() bool { return e.running }
func (e *fakeEngine) ConfigurePolicy(
context.Context,
string,
engine.LimitSet,
) (engine.PolicyConfigurationResult, error) {
return engine.PolicyConfigurationResult{}, nil
}
func (e *fakeEngine) SetSpotFundsTrackOnly(context.Context, bool) error {
return nil
}
func (*fakeEngine) AddAccountResolverEntry(domain.Account) error { return nil }
func (*fakeEngine) AddAssetResolverEntry(domain.Asset) error { return nil }
func (*fakeEngine) AddGroupResolverEntry(domain.AccountGroup) error {
return nil
}
func (*fakeEngine) RenameAccountResolverEntry(domain.AccountID, domain.Account) error {
return nil
}
func (*fakeEngine) RenameAssetResolverEntry(string, domain.Asset) error { return nil }
func (*fakeEngine) RenameGroupResolverEntry(string, domain.AccountGroup) error {
return nil
}
func (*fakeEngine) RemoveAssetResolverEntry(domain.Asset) error { return nil }
func (*fakeEngine) RemoveGroupResolverEntry(domain.AccountGroup) error {
return nil
}
func (*fakeEngine) ResolveAsset(string) (param.Asset, error) {
return param.Asset{}, errUnexpectedOrderChain
}
func (*fakeEngine) ResolveGroup(string) (param.AccountGroupID, error) {
return param.AccountGroupID{}, errUnexpectedOrderChain
}
func (e *fakeEngine) MarketDataSink() marketdata.Sink { return e.sink }
func (e *fakeEngine) Stop() {
e.running = false
e.lifecycle = append(e.lifecycle, "stop")
}
func (e *fakeEngine) CloseMarketDataService() {
e.lifecycle = append(e.lifecycle, "close market data")
}