# Admin Trade Control - Test Plan
## Test Objectives
Verify that the Admin Trade Control feature:
1. ✅ Blocks new trade entries when paused
2. ✅ Allows existing positions to continue lifecycle
3. ✅ Only allows admin users to control trading state
4. ✅ Persists state across restarts
5. ✅ Provides accurate UI feedback
6. ✅ Handles errors gracefully
## Backend Tests
### Unit Tests
#### HealthTracker Tests
**File**: `bytelyst-trading-bot-service/src/services/healthTracker.test.ts`
```typescript
import { HealthTracker } from './healthTracker';
describe('HealthTracker - Trading Control', () => {
let tracker: HealthTracker;
beforeEach(() => {
tracker = new HealthTracker();
});
test('should default to RUNNING mode', () => {
const snapshot = tracker.getSnapshot();
expect(snapshot.tradingControl.mode).toBe('RUNNING');
expect(tracker.isPaused()).toBe(false);
});
test('should pause trading when mode set to PAUSED', () => {
tracker.recordTradingControl({
mode: 'PAUSED',
lastChangedBy: 'admin@test.com',
lastChangedAt: Date.now(),
reason: 'Test pause'
});
expect(tracker.isPaused()).toBe(true);
});
test('should resume trading when mode set to RUNNING', () => {
tracker.recordTradingControl({ mode: 'PAUSED', lastChangedBy: 'admin', lastChangedAt: Date.now() });
tracker.recordTradingControl({ mode: 'RUNNING', lastChangedBy: 'admin', lastChangedAt: Date.now() });
expect(tracker.isPaused()).toBe(false);
});
test('should record who changed the state', () => {
const userId = 'admin@test.com';
tracker.recordTradingControl({
mode: 'PAUSED',
lastChangedBy: userId,
lastChangedAt: Date.now()
});
const snapshot = tracker.getSnapshot();
expect(snapshot.tradingControl.lastChangedBy).toBe(userId);
});
test('should record timestamp of change', () => {
const now = Date.now();
tracker.recordTradingControl({
mode: 'PAUSED',
lastChangedBy: 'admin',
lastChangedAt: now
});
const snapshot = tracker.getSnapshot();
expect(snapshot.tradingControl.lastChangedAt).toBe(now);
});
});
```
#### AutoTrader Tests
**File**: `bytelyst-trading-bot-service/src/services/AutoTrader.test.ts`
```typescript
import { AutoTrader } from './AutoTrader';
import { healthTracker } from './healthTracker';
describe('AutoTrader - Pause Enforcement', () => {
let autoTrader: AutoTrader;
let mockExecutor: any;
let mockExchange: any;
beforeEach(() => {
mockExecutor = {
getActivePositions: jest.fn(() => []),
getOpenPositionCount: jest.fn(() => 0),
checkCooldown: jest.fn(() => false),
openPosition: jest.fn()
};
mockExchange = {
getPosition: jest.fn(() => null)
};
autoTrader = new AutoTrader(mockExecutor, mockExchange);
// Reset to RUNNING
healthTracker.recordTradingControl({
mode: 'RUNNING',
lastChangedBy: 'system',
lastChangedAt: Date.now()
});
});
test('should block entry when paused', async () => {
healthTracker.recordTradingControl({
mode: 'PAUSED',
lastChangedBy: 'admin',
lastChangedAt: Date.now()
});
const result = { signal: 'BUY', passed: true };
const context = { currentPrice: 50000 };
await autoTrader.handleSignal('BTC/USDT', result, context);
expect(mockExecutor.openPosition).not.toHaveBeenCalled();
});
test('should allow entry when running', async () => {
const result = { signal: 'BUY', passed: true };
const context = { currentPrice: 50000 };
await autoTrader.handleSignal('BTC/USDT', result, context);
// Entry logic should proceed (may be blocked by other checks)
// At minimum, pause check should not block
});
test('should still close positions when paused', async () => {
healthTracker.recordTradingControl({
mode: 'PAUSED',
lastChangedBy: 'admin',
lastChangedAt: Date.now()
});
const activePosition = {
side: 'BUY',
entryPrice: 50000,
size: 1,
peakPrice: 51000
};
mockExecutor.getActivePositions.mockReturnValue([activePosition]);
mockExecutor.closePosition = jest.fn();
const result = { signal: 'SELL', passed: true };
const context = { currentPrice: 51000 };
await autoTrader.handleSignal('BTC/USDT', result, context);
expect(mockExecutor.closePosition).toHaveBeenCalled();
});
});
```
#### TradeExecutor Tests
**File**: `bytelyst-trading-bot-service/src/services/TradeExecutor.test.ts`
```typescript
import { TradeExecutor } from './TradeExecutor';
import { healthTracker } from './healthTracker';
describe('TradeExecutor - Pause Enforcement', () => {
let executor: TradeExecutor;
let mockExchange: any;
beforeEach(() => {
mockExchange = {
placeOrder: jest.fn()
};
executor = new TradeExecutor(mockExchange);
healthTracker.recordTradingControl({
mode: 'RUNNING',
lastChangedBy: 'system',
lastChangedAt: Date.now()
});
});
test('should block openPosition when paused', async () => {
healthTracker.recordTradingControl({
mode: 'PAUSED',
lastChangedBy: 'admin',
lastChangedAt: Date.now()
});
const result = await executor.openPosition('BTC/USDT', 'BUY', 1, 'market', 50000);
expect(result.success).toBe(false);
expect(result.error).toContain('paused');
expect(mockExchange.placeOrder).not.toHaveBeenCalled();
});
test('should allow openPosition when running', async () => {
mockExchange.placeOrder.mockResolvedValue({
id: 'order-123',
status: 'filled',
filled_avg_price: 50000
});
const result = await executor.openPosition('BTC/USDT', 'BUY', 1, 'market', 50000);
expect(mockExchange.placeOrder).toHaveBeenCalled();
});
});
```
### Integration Tests
**File**: `bytelyst-trading-bot-service/tests/integration/tradingControl.test.ts`
```typescript
describe('Trading Control Integration', () => {
test('pause → no new entries → resume → entries allowed', async () => {
// 1. Pause trading
await request(app)
.post('/internal/trading/pause')
.set('Authorization', `Bearer ${adminToken}`)
.send({ reason: 'Integration test' })
.expect(200);
// 2. Attempt to place entry (should be blocked)
const entryResult = await autoTrader.handleSignal('BTC/USDT', buySignal, context);
expect(entryResult).toBeUndefined(); // blocked
// 3. Resume trading
await request(app)
.post('/internal/trading/resume')
.set('Authorization', `Bearer ${adminToken}`)
.send({ reason: 'Integration test' })
.expect(200);
// 4. Attempt to place entry (should succeed)
const entryResult2 = await autoTrader.handleSignal('BTC/USDT', buySignal, context);
expect(entryResult2).toBeDefined(); // allowed
});
test('paused state persists across restart', async () => {
// 1. Pause trading
await request(app)
.post('/internal/trading/pause')
.set('Authorization', `Bearer ${adminToken}`)
.send({ reason: 'Persistence test' })
.expect(200);
// 2. Simulate restart (reload state)
apiServer.loadState();
// 3. Verify still paused
const status = await request(app)
.get('/internal/trading/status')
.set('Authorization', `Bearer ${adminToken}`)
.expect(200);
expect(status.body.mode).toBe('PAUSED');
});
});
```
## Frontend Tests
### Component Tests
**File**: `bytelyst-trading-dashboard-web/src/tabs/AdminTab.test.tsx`
```typescript
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { AdminTab } from './AdminTab';
describe('AdminTab - Trading Control', () => {
const mockBotState = {
health: {
tradingControl: {
mode: 'RUNNING',
lastChangedBy: 'system',
lastChangedAt: Date.now()
}
},
settings: { enabledRules: [] }
};
test('should show running status when mode is RUNNING', () => {
render();
expect(screen.getByText(/AUTO-TRADING: RUNNING/i)).toBeInTheDocument();
});
test('should show paused status when mode is PAUSED', () => {
const pausedState = {
...mockBotState,
health: {
tradingControl: {
mode: 'PAUSED',
lastChangedBy: 'admin@test.com',
lastChangedAt: Date.now()
}
}
};
render();
expect(screen.getByText(/AUTO-TRADING: PAUSED/i)).toBeInTheDocument();
});
test('should disable pause button when already paused', () => {
const pausedState = {
...mockBotState,
health: {
tradingControl: {
mode: 'PAUSED',
lastChangedBy: 'admin',
lastChangedAt: Date.now()
}
}
};
render();
const pauseButton = screen.getByText(/Pause Auto Trading/i);
expect(pauseButton).toBeDisabled();
});
test('should disable resume button when already running', () => {
render();
const resumeButton = screen.getByText(/Resume Auto Trading/i);
expect(resumeButton).toBeDisabled();
});
test('should show loading state when API call in progress', async () => {
global.fetch = jest.fn(() => new Promise(() => {})); // Never resolves
render();
const pauseButton = screen.getByText(/Pause Auto Trading/i);
fireEvent.click(pauseButton);
await waitFor(() => {
expect(screen.getByText(/Pausing.../i)).toBeInTheDocument();
});
});
test('should show error when API call fails', async () => {
global.fetch = jest.fn(() =>
Promise.resolve({
ok: false,
json: () => Promise.resolve({ error: 'Unauthorized' })
})
);
render();
const pauseButton = screen.getByText(/Pause Auto Trading/i);
fireEvent.click(pauseButton);
await waitFor(() => {
expect(screen.getByText(/Unauthorized/i)).toBeInTheDocument();
});
});
test('should display safety notice', () => {
render();
expect(screen.getByText(/Safety Note:/i)).toBeInTheDocument();
expect(screen.getByText(/Existing positions will continue/i)).toBeInTheDocument();
});
});
```
### App Header Tests
**File**: `bytelyst-trading-dashboard-web/src/App.test.tsx`
```typescript
describe('App - Trading Control Header Badge', () => {
test('should show trading active badge when running', () => {
const runningState = {
health: {
tradingControl: {
mode: 'RUNNING',
lastChangedBy: 'system',
lastChangedAt: Date.now()
}
}
};
render();
expect(screen.getByText(/Trading Active/i)).toBeInTheDocument();
});
test('should show trading paused badge when paused', () => {
const pausedState = {
health: {
tradingControl: {
mode: 'PAUSED',
lastChangedBy: 'admin',
lastChangedAt: Date.now()
}
}
};
render();
expect(screen.getByText(/Trading Paused/i)).toBeInTheDocument();
});
test('should show tooltip with pause details', () => {
const pausedState = {
health: {
tradingControl: {
mode: 'PAUSED',
lastChangedBy: 'admin@test.com',
lastChangedAt: Date.now()
}
}
};
render();
const badge = screen.getByText(/Trading Paused/i).closest('div');
expect(badge).toHaveAttribute('title', expect.stringContaining('admin@test.com'));
});
});
```
## API Tests
### Security Tests
```typescript
describe('Trading Control API - Security', () => {
test('should reject pause request without auth token', async () => {
await request(app)
.post('/internal/trading/pause')
.send({ reason: 'Test' })
.expect(401);
});
test('should reject pause request from non-admin user', async () => {
await request(app)
.post('/internal/trading/pause')
.set('Authorization', `Bearer ${regularUserToken}`)
.send({ reason: 'Test' })
.expect(403);
});
test('should allow pause request from admin user', async () => {
await request(app)
.post('/internal/trading/pause')
.set('Authorization', `Bearer ${adminToken}`)
.send({ reason: 'Test' })
.expect(200);
});
test('should allow status check from any authenticated user', async () => {
await request(app)
.get('/internal/trading/status')
.set('Authorization', `Bearer ${regularUserToken}`)
.expect(200);
});
});
```
### Idempotency Tests
```typescript
describe('Trading Control API - Idempotency', () => {
test('should be idempotent when pausing already paused system', async () => {
// Pause once
const res1 = await request(app)
.post('/internal/trading/pause')
.set('Authorization', `Bearer ${adminToken}`)
.send({ reason: 'Test' })
.expect(200);
// Pause again
const res2 = await request(app)
.post('/internal/trading/pause')
.set('Authorization', `Bearer ${adminToken}`)
.send({ reason: 'Test' })
.expect(200);
expect(res1.body.status.mode).toBe('PAUSED');
expect(res2.body.status.mode).toBe('PAUSED');
});
test('should be idempotent when resuming already running system', async () => {
// Resume once
const res1 = await request(app)
.post('/internal/trading/resume')
.set('Authorization', `Bearer ${adminToken}`)
.send({ reason: 'Test' })
.expect(200);
// Resume again
const res2 = await request(app)
.post('/internal/trading/resume')
.set('Authorization', `Bearer ${adminToken}`)
.send({ reason: 'Test' })
.expect(200);
expect(res1.body.status.mode).toBe('RUNNING');
expect(res2.body.status.mode).toBe('RUNNING');
});
});
```
## Manual Testing Checklist
### Backend Testing
- [ ] Start bot in RUNNING mode
- [ ] Call `/internal/trading/pause` as admin → verify success
- [ ] Attempt to place entry order → verify blocked
- [ ] Verify existing position still monitored
- [ ] Call `/internal/trading/resume` as admin → verify success
- [ ] Attempt to place entry order → verify allowed
- [ ] Restart bot → verify state persisted
- [ ] Call `/internal/trading/pause` as non-admin → verify 403
- [ ] Call `/internal/trading/status` → verify correct state returned
### Frontend Testing
- [ ] Login as admin user
- [ ] Navigate to Admin tab
- [ ] Verify Trading Control section visible
- [ ] Click "Pause Auto Trading" → verify button disabled, status updates
- [ ] Verify header badge shows "Trading Paused"
- [ ] Navigate to other tabs → verify header badge still visible
- [ ] Click "Resume Auto Trading" → verify button disabled, status updates
- [ ] Verify header badge shows "Trading Active"
- [ ] Simulate API error → verify error message displayed
- [ ] Login as non-admin user → verify Trading Control section hidden
### Edge Cases
- [ ] Pause while entry order is pending → verify order completes
- [ ] Pause while exit order is pending → verify order completes
- [ ] WebSocket disconnect while paused → verify status persists on reconnect
- [ ] Multiple admins pause/resume simultaneously → verify last write wins
- [ ] Bot restart while paused → verify still paused after restart
## Performance Testing
- [ ] Measure latency of pause/resume API calls
- [ ] Verify no performance impact on trading loop when paused
- [ ] Verify no performance impact on position monitoring when paused
- [ ] Test with 100+ concurrent pause/resume requests
## Acceptance Criteria
✅ **All tests pass**
✅ **No new entries when paused**
✅ **Existing positions continue lifecycle**
✅ **Only admins can pause/resume**
✅ **State persists across restarts**
✅ **UI accurately reflects backend state**
✅ **Errors handled gracefully**
✅ **Audit logs capture all changes**
## Test Execution
Run backend tests:
```bash
cd bytelyst-trading-bot-service
npm test -- --testPathPattern=healthTracker
npm test -- --testPathPattern=AutoTrader
npm test -- --testPathPattern=TradeExecutor
```
Run frontend tests:
```bash
cd bytelyst-trading-dashboard-web
npm test -- --testPathPattern=AdminTab
npm test -- --testPathPattern=App
```
Run integration tests:
```bash
cd bytelyst-trading-bot-service
npm run test:integration
```