@boundless-oss/atlas
Version:
Atlas - MCP Server for comprehensive startup project management
290 lines (245 loc) • 9.01 kB
text/typescript
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');
});
});
});