UNPKG

@boundless-oss/atlas

Version:

Atlas - MCP Server for comprehensive startup project management

290 lines (245 loc) 9.01 kB
import { describe, it, expect, beforeEach, vi } from 'vitest'; import request from 'supertest'; import express from 'express'; import { setupApprovalsAPI } from '../../api/approvals.js'; import { getSQLiteManager, ensureDatabaseReady } from '../../../storage/sqlite-manager.js'; // Mock the SQLite manager vi.mock('../../../storage/sqlite-manager.js', () => ({ getSQLiteManager: vi.fn(), ensureDatabaseReady: vi.fn() })); const mockSQLiteManager = { query: vi.fn(), get: vi.fn(), run: vi.fn(), initialize: vi.fn(), close: vi.fn() }; describe('Approvals API', () => { let app: express.Application; beforeEach(() => { app = express(); app.use(express.json()); // Reset mocks vi.clearAllMocks(); vi.mocked(getSQLiteManager).mockReturnValue(mockSQLiteManager as any); vi.mocked(ensureDatabaseReady).mockResolvedValue(mockSQLiteManager as any); // Setup the approvals API setupApprovalsAPI(app); }); describe('GET /api/approvals', () => { it('should return all approval requests', async () => { const mockApprovals = [ { id: 'approval-123', action: 'create_sprint', context: '{"sprintName": "Sprint 1"}', description: 'Create new sprint', urgency: 'medium', status: 'pending', requested_by: 'user-1', requested_at: Date.now(), resolved_by: null, resolved_at: null, timeout_at: Date.now() + 3600000, response_data: null, notes: null } ]; mockSQLiteManager.query.mockResolvedValue({ success: true, data: mockApprovals }); const response = await request(app) .get('/api/approvals') .expect(200); expect(response.body.success).toBe(true); expect(response.body.data.approvals).toHaveLength(1); expect(response.body.data.approvals[0].id).toBe('approval-123'); expect(response.body.data.approvals[0].action).toBe('create_sprint'); expect(response.body.data.approvals[0].context).toEqual({ sprintName: 'Sprint 1' }); }); it('should filter by status', async () => { mockSQLiteManager.query.mockResolvedValue({ success: true, data: [] }); await request(app) .get('/api/approvals?status=pending') .expect(200); expect(mockSQLiteManager.query).toHaveBeenCalledWith( expect.stringContaining('AND status = ?'), expect.arrayContaining(['pending']) ); }); it('should handle database errors', async () => { mockSQLiteManager.query.mockResolvedValue({ success: false, error: 'Database connection failed' }); const response = await request(app) .get('/api/approvals') .expect(500); expect(response.body.success).toBe(false); expect(response.body.error).toBe('Failed to fetch approval requests'); }); }); describe('GET /api/approvals/:id', () => { it('should return specific approval request', async () => { const mockApproval = { id: 'approval-123', action: 'create_sprint', context: '{"sprintName": "Sprint 1"}', description: 'Create new sprint', urgency: 'medium', status: 'pending', requested_by: 'user-1', requested_at: Date.now(), resolved_by: null, resolved_at: null, timeout_at: Date.now() + 3600000, response_data: null, notes: null }; mockSQLiteManager.get.mockResolvedValue({ success: true, data: mockApproval }); const response = await request(app) .get('/api/approvals/approval-123') .expect(200); expect(response.body.success).toBe(true); expect(response.body.data.id).toBe('approval-123'); expect(response.body.data.context).toEqual({ sprintName: 'Sprint 1' }); }); it('should return 404 for non-existent approval', async () => { mockSQLiteManager.get.mockResolvedValue({ success: true, data: null }); const response = await request(app) .get('/api/approvals/non-existent') .expect(404); expect(response.body.success).toBe(false); expect(response.body.error).toBe('Approval request not found'); }); }); describe('POST /api/approvals/:id/respond', () => { it('should approve an approval request', async () => { mockSQLiteManager.run.mockResolvedValue({ success: true, data: { changes: 1 } }); const response = await request(app) .post('/api/approvals/approval-123/respond') .send({ decision: 'approved', notes: 'Looks good', resolvedBy: 'admin-user' }) .expect(200); expect(response.body.success).toBe(true); expect(response.body.data.decision).toBe('approved'); expect(response.body.data.resolvedBy).toBe('admin-user'); }); it('should reject invalid decisions', async () => { const response = await request(app) .post('/api/approvals/approval-123/respond') .send({ decision: 'invalid' }) .expect(400); expect(response.body.success).toBe(false); expect(response.body.error).toBe('Invalid decision. Must be "approved" or "rejected"'); }); it('should handle already resolved requests', async () => { mockSQLiteManager.run.mockResolvedValue({ success: true, data: { changes: 0 } }); const response = await request(app) .post('/api/approvals/approval-123/respond') .send({ decision: 'approved' }) .expect(404); expect(response.body.success).toBe(false); expect(response.body.error).toBe('Approval request not found or already resolved'); }); }); describe('GET /api/approvals/stats', () => { it('should return approval statistics', async () => { // Mock individual count queries mockSQLiteManager.get .mockResolvedValueOnce({ success: true, data: { total: 10 } }) .mockResolvedValueOnce({ success: true, data: { pending: 3 } }) .mockResolvedValueOnce({ success: true, data: { approved: 5 } }) .mockResolvedValueOnce({ success: true, data: { rejected: 2 } }) .mockResolvedValueOnce({ success: true, data: { expired: 0 } }); // Mock urgency breakdown query mockSQLiteManager.query.mockResolvedValue({ success: true, data: [ { urgency: 'high', count: 4 }, { urgency: 'medium', count: 5 }, { urgency: 'low', count: 1 } ] }); const response = await request(app) .get('/api/approvals/stats') .expect(200); expect(response.body.success).toBe(true); expect(response.body.data.total).toBe(10); expect(response.body.data.pending).toBe(3); expect(response.body.data.approved).toBe(5); expect(response.body.data.urgencyBreakdown).toEqual({ high: 4, medium: 5, low: 1 }); }); it('should handle different timeframes', async () => { // Setup mock responses for all stats queries in sequence mockSQLiteManager.get .mockResolvedValueOnce({ success: true, data: { total: 5 } }) .mockResolvedValueOnce({ success: true, data: { pending: 2 } }) .mockResolvedValueOnce({ success: true, data: { approved: 2 } }) .mockResolvedValueOnce({ success: true, data: { rejected: 1 } }) .mockResolvedValueOnce({ success: true, data: { expired: 0 } }); mockSQLiteManager.query.mockResolvedValue({ success: true, data: [] }); await request(app) .get('/api/approvals/stats?timeframe=24h') .expect(200); expect(mockSQLiteManager.get).toHaveBeenCalledWith( expect.stringContaining('WHERE requested_at > ?'), expect.arrayContaining([expect.any(Number)]) ); }); }); describe('GET /api/approvals/health', () => { it('should return healthy status', async () => { // Provide a simple mock that won't trigger the invalid timestamp conversion mockSQLiteManager.get.mockResolvedValue({ success: true, data: { count: 0 } }); const response = await request(app) .get('/api/approvals/health') .expect(200); expect(response.body.success).toBe(true); expect(response.body.status).toBe('healthy'); expect(response.body.data.databaseConnected).toBe(true); }); it('should return unhealthy status on database failure', async () => { mockSQLiteManager.get.mockResolvedValue({ success: false, error: 'Database connection failed' }); const response = await request(app) .get('/api/approvals/health') .expect(503); expect(response.body.success).toBe(false); expect(response.body.status).toBe('unhealthy'); }); }); });