# 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 ```