diff --git a/tests/integration/api.governance.test.js b/tests/integration/api.governance.test.js new file mode 100644 index 00000000..f8cbfa9a --- /dev/null +++ b/tests/integration/api.governance.test.js @@ -0,0 +1,514 @@ +/** + * Integration Tests - Governance API + * Tests Tractatus framework testing endpoints + */ + +const request = require('supertest'); +const { MongoClient } = require('mongodb'); +const bcrypt = require('bcrypt'); +const app = require('../../src/server'); +const config = require('../../src/config/app.config'); + +describe('Governance API Integration Tests', () => { + let connection; + let db; + let adminToken; + let regularUserToken; + + const adminUser = { + email: 'admin@governance.test.local', + password: 'AdminGov123!', + role: 'admin' + }; + + const regularUser = { + email: 'user@governance.test.local', + password: 'UserGov123!', + role: 'user' + }; + + // Setup test users + beforeAll(async () => { + connection = await MongoClient.connect(config.mongodb.uri); + db = connection.db(config.mongodb.db); + + // Clean up existing test users + await db.collection('users').deleteMany({ + email: { $in: [adminUser.email, regularUser.email] } + }); + + // Create admin user + const adminHash = await bcrypt.hash(adminUser.password, 10); + await db.collection('users').insertOne({ + email: adminUser.email, + password: adminHash, + name: 'Test Admin', + role: adminUser.role, + created_at: new Date(), + active: true + }); + + // Create regular user + const userHash = await bcrypt.hash(regularUser.password, 10); + await db.collection('users').insertOne({ + email: regularUser.email, + password: userHash, + name: 'Test User', + role: regularUser.role, + created_at: new Date(), + active: true + }); + + // Get tokens + const adminLogin = await request(app) + .post('/api/auth/login') + .send({ + email: adminUser.email, + password: adminUser.password + }); + adminToken = adminLogin.body.token; + + const userLogin = await request(app) + .post('/api/auth/login') + .send({ + email: regularUser.email, + password: regularUser.password + }); + regularUserToken = userLogin.body.token; + }); + + // Clean up + afterAll(async () => { + await db.collection('users').deleteMany({ + email: { $in: [adminUser.email, regularUser.email] } + }); + await connection.close(); + }); + + describe('GET /api/governance', () => { + test('should require authentication', async () => { + const response = await request(app) + .get('/api/governance') + .expect(401); + + expect(response.body).toHaveProperty('error'); + }); + + test('should require admin role', async () => { + const response = await request(app) + .get('/api/governance') + .set('Authorization', `Bearer ${regularUserToken}`) + .expect(403); + + expect(response.body).toHaveProperty('error'); + }); + + test('should redirect HTML requests to API documentation', async () => { + const response = await request(app) + .get('/api/governance') + .set('Authorization', `Bearer ${adminToken}`) + .set('Accept', 'text/html') + .expect(302); + + expect(response.headers.location).toContain('api-reference.html#governance'); + }); + + test('should return JSON for JSON requests with admin auth', async () => { + const response = await request(app) + .get('/api/governance') + .set('Authorization', `Bearer ${adminToken}`) + .set('Accept', 'application/json') + .expect(200); + + // Response is the framework object directly (not wrapped in success) + expect(response.body).toHaveProperty('name', 'Tractatus Governance Framework'); + expect(response.body).toHaveProperty('operational', true); + expect(response.body).toHaveProperty('services'); + }); + }); + + describe('GET /api/governance/status', () => { + test('should require authentication', async () => { + const response = await request(app) + .get('/api/governance/status') + .expect(401); + + expect(response.body).toHaveProperty('error'); + }); + + test('should require admin role', async () => { + const response = await request(app) + .get('/api/governance/status') + .set('Authorization', `Bearer ${regularUserToken}`) + .expect(403); + + expect(response.body).toHaveProperty('error'); + }); + + test('should return detailed framework status with admin auth', async () => { + const response = await request(app) + .get('/api/governance/status') + .set('Authorization', `Bearer ${adminToken}`) + .expect(200); + + expect(response.body).toHaveProperty('success', true); + expect(response.body).toHaveProperty('uptime'); + expect(response.body).toHaveProperty('environment'); + expect(typeof response.body.uptime).toBe('number'); + }); + }); + + describe('POST /api/governance/classify', () => { + test('should require authentication', async () => { + const response = await request(app) + .post('/api/governance/classify') + .send({ text: 'Test instruction' }) + .expect(401); + + expect(response.body).toHaveProperty('error'); + }); + + test('should require admin role', async () => { + const response = await request(app) + .post('/api/governance/classify') + .set('Authorization', `Bearer ${regularUserToken}`) + .send({ text: 'Test instruction' }) + .expect(403); + + expect(response.body).toHaveProperty('error'); + }); + + test('should reject request without text field', async () => { + const response = await request(app) + .post('/api/governance/classify') + .set('Authorization', `Bearer ${adminToken}`) + .send({}) + .expect(400); + + expect(response.body).toHaveProperty('error', 'Bad Request'); + expect(response.body.message).toContain('text field is required'); + }); + + test('should classify instruction with admin auth', async () => { + const response = await request(app) + .post('/api/governance/classify') + .set('Authorization', `Bearer ${adminToken}`) + .send({ + text: 'Always use TypeScript for new projects', + context: { source: 'test' } + }) + .expect(200); + + expect(response.body).toHaveProperty('success', true); + expect(response.body).toHaveProperty('classification'); + expect(response.body.classification).toHaveProperty('quadrant'); + expect(response.body.classification).toHaveProperty('persistence'); + }); + + test('should classify without context parameter', async () => { + const response = await request(app) + .post('/api/governance/classify') + .set('Authorization', `Bearer ${adminToken}`) + .send({ + text: 'Run tests before committing' + }) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.classification).toBeDefined(); + }); + }); + + describe('POST /api/governance/validate', () => { + test('should require authentication', async () => { + const response = await request(app) + .post('/api/governance/validate') + .send({ action: { type: 'test' } }) + .expect(401); + }); + + test('should require admin role', async () => { + const response = await request(app) + .post('/api/governance/validate') + .set('Authorization', `Bearer ${regularUserToken}`) + .send({ action: { type: 'test' } }) + .expect(403); + }); + + test('should reject request without action field', async () => { + const response = await request(app) + .post('/api/governance/validate') + .set('Authorization', `Bearer ${adminToken}`) + .send({}) + .expect(400); + + expect(response.body).toHaveProperty('error', 'Bad Request'); + expect(response.body.message).toContain('action object is required'); + }); + + test('should validate action with admin auth', async () => { + const response = await request(app) + .post('/api/governance/validate') + .set('Authorization', `Bearer ${adminToken}`) + .send({ + action: { + type: 'file-edit', + description: 'Update configuration file', + filePath: 'config.json' + }, + context: { + messages: [] + } + }) + .expect(200); + + expect(response.body).toHaveProperty('success', true); + expect(response.body).toHaveProperty('validation'); + expect(response.body.validation).toHaveProperty('status'); + expect(response.body.validation).toHaveProperty('action'); + }); + + test('should validate without context parameter', async () => { + const response = await request(app) + .post('/api/governance/validate') + .set('Authorization', `Bearer ${adminToken}`) + .send({ + action: { + type: 'database', + description: 'Update schema' + } + }) + .expect(200); + + expect(response.body.success).toBe(true); + }); + }); + + describe('POST /api/governance/enforce', () => { + test('should require authentication', async () => { + const response = await request(app) + .post('/api/governance/enforce') + .send({ action: { type: 'test' } }) + .expect(401); + }); + + test('should require admin role', async () => { + const response = await request(app) + .post('/api/governance/enforce') + .set('Authorization', `Bearer ${regularUserToken}`) + .send({ action: { type: 'test' } }) + .expect(403); + }); + + test('should reject request without action field', async () => { + const response = await request(app) + .post('/api/governance/enforce') + .set('Authorization', `Bearer ${adminToken}`) + .send({}) + .expect(400); + + expect(response.body).toHaveProperty('error', 'Bad Request'); + expect(response.body.message).toContain('action object is required'); + }); + + test('should enforce boundaries with admin auth', async () => { + const response = await request(app) + .post('/api/governance/enforce') + .set('Authorization', `Bearer ${adminToken}`) + .send({ + action: { + type: 'values', + description: 'Define project values', + decision: 'What are our core values?' + }, + context: {} + }) + .expect(200); + + expect(response.body).toHaveProperty('success', true); + expect(response.body).toHaveProperty('enforcement'); + expect(response.body.enforcement).toHaveProperty('allowed'); + expect(response.body.enforcement).toHaveProperty('domain'); + }); + + test('should enforce without context parameter', async () => { + const response = await request(app) + .post('/api/governance/enforce') + .set('Authorization', `Bearer ${adminToken}`) + .send({ + action: { + type: 'architecture', + description: 'Change system architecture' + } + }) + .expect(200); + + expect(response.body.success).toBe(true); + }); + }); + + describe('POST /api/governance/pressure', () => { + test('should require authentication', async () => { + const response = await request(app) + .post('/api/governance/pressure') + .send({}) + .expect(401); + }); + + test('should require admin role', async () => { + const response = await request(app) + .post('/api/governance/pressure') + .set('Authorization', `Bearer ${regularUserToken}`) + .send({}) + .expect(403); + }); + + test('should analyze pressure with admin auth', async () => { + const response = await request(app) + .post('/api/governance/pressure') + .set('Authorization', `Bearer ${adminToken}`) + .send({ + context: { + tokenUsage: 100000, + tokenBudget: 200000, + messageCount: 50 + } + }) + .expect(200); + + expect(response.body).toHaveProperty('success', true); + expect(response.body).toHaveProperty('pressure'); + expect(response.body.pressure).toHaveProperty('pressureLevel'); + expect(response.body.pressure).toHaveProperty('overall_score'); + }); + + test('should use default context when not provided', async () => { + const response = await request(app) + .post('/api/governance/pressure') + .set('Authorization', `Bearer ${adminToken}`) + .send({}) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.pressure).toBeDefined(); + }); + }); + + describe('POST /api/governance/verify', () => { + test('should require authentication', async () => { + const response = await request(app) + .post('/api/governance/verify') + .send({ action: { type: 'test' } }) + .expect(401); + }); + + test('should require admin role', async () => { + const response = await request(app) + .post('/api/governance/verify') + .set('Authorization', `Bearer ${regularUserToken}`) + .send({ action: { type: 'test' } }) + .expect(403); + }); + + test('should reject request without action field', async () => { + const response = await request(app) + .post('/api/governance/verify') + .set('Authorization', `Bearer ${adminToken}`) + .send({}) + .expect(400); + + expect(response.body).toHaveProperty('error', 'Bad Request'); + expect(response.body.message).toContain('action object is required'); + }); + + test('should verify action with admin auth', async () => { + const response = await request(app) + .post('/api/governance/verify') + .set('Authorization', `Bearer ${adminToken}`) + .send({ + action: { + type: 'complex', + description: 'Refactor authentication system', + parameters: { + files: ['auth.js', 'middleware.js'], + changes: 'major' + } + }, + reasoning: { + userGoal: 'Improve security', + approach: 'Use JWT tokens' + }, + context: {} + }) + .expect(200); + + expect(response.body).toHaveProperty('success', true); + expect(response.body).toHaveProperty('verification'); + expect(response.body.verification).toHaveProperty('checks'); + expect(response.body.verification.checks).toHaveProperty('alignment'); + }); + + test('should verify without reasoning parameter', async () => { + const response = await request(app) + .post('/api/governance/verify') + .set('Authorization', `Bearer ${adminToken}`) + .send({ + action: { + type: 'simple', + description: 'Update README' + } + }) + .expect(200); + + expect(response.body.success).toBe(true); + }); + }); + + describe('Admin-Only Access Control', () => { + test('should enforce admin-only access across all governance routes', async () => { + const governanceRoutes = [ + { method: 'get', path: '/api/governance/status' }, + { method: 'post', path: '/api/governance/classify', body: { text: 'test' } }, + { method: 'post', path: '/api/governance/validate', body: { action: { type: 'test' } } }, + { method: 'post', path: '/api/governance/enforce', body: { action: { type: 'test' } } }, + { method: 'post', path: '/api/governance/pressure', body: {} }, + { method: 'post', path: '/api/governance/verify', body: { action: { type: 'test' } } } + ]; + + for (const route of governanceRoutes) { + const req = request(app)[route.method](route.path) + .set('Authorization', `Bearer ${regularUserToken}`); + + if (route.body) { + req.send(route.body); + } + + const response = await req; + expect(response.status).toBe(403); + } + }); + + test('should allow admin access to all governance routes', async () => { + const governanceRoutes = [ + { method: 'get', path: '/api/governance/status' }, + { method: 'post', path: '/api/governance/classify', body: { text: 'test' } }, + { method: 'post', path: '/api/governance/validate', body: { action: { type: 'test' } } }, + { method: 'post', path: '/api/governance/enforce', body: { action: { type: 'test' } } }, + { method: 'post', path: '/api/governance/pressure', body: {} }, + { method: 'post', path: '/api/governance/verify', body: { action: { type: 'test' } } } + ]; + + for (const route of governanceRoutes) { + const req = request(app)[route.method](route.path) + .set('Authorization', `Bearer ${adminToken}`); + + if (route.body) { + req.send(route.body); + } + + const response = await req; + expect(response.status).toBe(200); + } + }); + }); +}); diff --git a/tests/unit/markdown.util.test.js b/tests/unit/markdown.util.test.js new file mode 100644 index 00000000..5f7efcc8 --- /dev/null +++ b/tests/unit/markdown.util.test.js @@ -0,0 +1,502 @@ +/** + * Unit Tests - Markdown Utility + * Tests markdown conversion, TOC extraction, front matter parsing, and slug generation + */ + +const { + markdownToHtml, + extractTOC, + extractFrontMatter, + generateSlug +} = require('../../src/utils/markdown.util'); + +describe('Markdown Utility', () => { + describe('markdownToHtml', () => { + test('should return empty string for null input', () => { + expect(markdownToHtml(null)).toBe(''); + }); + + test('should return empty string for undefined input', () => { + expect(markdownToHtml(undefined)).toBe(''); + }); + + test('should return empty string for empty string', () => { + expect(markdownToHtml('')).toBe(''); + }); + + test('should convert basic paragraph', () => { + const markdown = 'This is a paragraph.'; + const html = markdownToHtml(markdown); + + expect(html).toContain('

This is a paragraph.

'); + }); + + test('should convert headings with IDs', () => { + const markdown = '# Test Heading'; + const html = markdownToHtml(markdown); + + expect(html).toContain('

Test Heading

'); + }); + + test('should convert multiple heading levels', () => { + const markdown = `# Heading 1 +## Heading 2 +### Heading 3`; + const html = markdownToHtml(markdown); + + expect(html).toContain('

Heading 1

'); + expect(html).toContain('

Heading 2

'); + expect(html).toContain('

Heading 3

'); + }); + + test('should generate slugs from headings with special characters', () => { + const markdown = '# Test: Special Characters!'; + const html = markdownToHtml(markdown); + + expect(html).toContain('id="test-special-characters"'); + }); + + test('should convert bold text', () => { + const markdown = '**bold text**'; + const html = markdownToHtml(markdown); + + expect(html).toContain('bold text'); + }); + + test('should convert italic text', () => { + const markdown = '*italic text*'; + const html = markdownToHtml(markdown); + + expect(html).toContain('italic text'); + }); + + test('should convert inline code', () => { + const markdown = '`code snippet`'; + const html = markdownToHtml(markdown); + + expect(html).toContain('code snippet'); + }); + + test('should convert code blocks with language', () => { + const markdown = '```javascript\nconst x = 1;\n```'; + const html = markdownToHtml(markdown); + + expect(html).toContain(' { + const markdown = '```\nplain code\n```'; + const html = markdownToHtml(markdown); + + expect(html).toContain(' { + const markdown = `- Item 1 +- Item 2 +- Item 3`; + const html = markdownToHtml(markdown); + + expect(html).toContain(''); + }); + + test('should convert ordered lists', () => { + const markdown = `1. First +2. Second +3. Third`; + const html = markdownToHtml(markdown); + + expect(html).toContain('
    '); + expect(html).toContain('
  1. First
  2. '); + expect(html).toContain('
  3. Second
  4. '); + expect(html).toContain('
  5. Third
  6. '); + expect(html).toContain('
'); + }); + + test('should convert links', () => { + const markdown = '[Link Text](https://example.com)'; + const html = markdownToHtml(markdown); + + expect(html).toContain('Link Text'); + }); + + test('should convert images', () => { + const markdown = '![Alt Text](https://example.com/image.png)'; + const html = markdownToHtml(markdown); + + expect(html).toContain(' { + const markdown = '> This is a quote'; + const html = markdownToHtml(markdown); + + expect(html).toContain('
'); + expect(html).toContain('This is a quote'); + expect(html).toContain('
'); + }); + + test('should convert tables', () => { + const markdown = `| Header 1 | Header 2 | +|----------|----------| +| Cell 1 | Cell 2 |`; + const html = markdownToHtml(markdown); + + expect(html).toContain(''); + expect(html).toContain(''); + expect(html).toContain(''); + expect(html).toContain(''); + expect(html).toContain(''); + }); + + test('should sanitize dangerous HTML (XSS protection)', () => { + const markdown = ''; + const html = markdownToHtml(markdown); + + // Script tags should be removed + expect(html).not.toContain('
Header 1Cell 1