test: add comprehensive coverage for governance and markdown utilities
Coverage Improvements (Task 3 - Week 1): - governance.routes.js: 31.81% → 100% (+68.19%) - markdown.util.js: 17.39% → 89.13% (+71.74%) New Test Files: - tests/integration/api.governance.test.js (33 tests) - Authentication/authorization for all 6 governance endpoints - Request validation (missing fields, invalid input) - Admin-only access control enforcement - Framework component testing (classify, validate, enforce, pressure, verify) - tests/unit/markdown.util.test.js (60 tests) - markdownToHtml: conversion, syntax highlighting, XSS sanitization (23 tests) - extractTOC: heading extraction and slug generation (11 tests) - extractFrontMatter: YAML front matter parsing (10 tests) - generateSlug: URL-safe slug generation (16 tests) This completes Week 1, Task 3: Increase test coverage on critical services. Previous tasks in same session: - Task 1: Fixed 29 production test failures ✓ - Task 2: Completed Koha security implementation ✓ 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
fb85dd3732
commit
42f0bc7d8c
2 changed files with 1016 additions and 0 deletions
514
tests/integration/api.governance.test.js
Normal file
514
tests/integration/api.governance.test.js
Normal file
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
502
tests/unit/markdown.util.test.js
Normal file
502
tests/unit/markdown.util.test.js
Normal file
|
|
@ -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('<p>This is a paragraph.</p>');
|
||||
});
|
||||
|
||||
test('should convert headings with IDs', () => {
|
||||
const markdown = '# Test Heading';
|
||||
const html = markdownToHtml(markdown);
|
||||
|
||||
expect(html).toContain('<h1 id="test-heading">Test Heading</h1>');
|
||||
});
|
||||
|
||||
test('should convert multiple heading levels', () => {
|
||||
const markdown = `# Heading 1
|
||||
## Heading 2
|
||||
### Heading 3`;
|
||||
const html = markdownToHtml(markdown);
|
||||
|
||||
expect(html).toContain('<h1 id="heading-1">Heading 1</h1>');
|
||||
expect(html).toContain('<h2 id="heading-2">Heading 2</h2>');
|
||||
expect(html).toContain('<h3 id="heading-3">Heading 3</h3>');
|
||||
});
|
||||
|
||||
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('<strong>bold text</strong>');
|
||||
});
|
||||
|
||||
test('should convert italic text', () => {
|
||||
const markdown = '*italic text*';
|
||||
const html = markdownToHtml(markdown);
|
||||
|
||||
expect(html).toContain('<em>italic text</em>');
|
||||
});
|
||||
|
||||
test('should convert inline code', () => {
|
||||
const markdown = '`code snippet`';
|
||||
const html = markdownToHtml(markdown);
|
||||
|
||||
expect(html).toContain('<code>code snippet</code>');
|
||||
});
|
||||
|
||||
test('should convert code blocks with language', () => {
|
||||
const markdown = '```javascript\nconst x = 1;\n```';
|
||||
const html = markdownToHtml(markdown);
|
||||
|
||||
expect(html).toContain('<pre');
|
||||
expect(html).toContain('<code');
|
||||
});
|
||||
|
||||
test('should convert code blocks without language', () => {
|
||||
const markdown = '```\nplain code\n```';
|
||||
const html = markdownToHtml(markdown);
|
||||
|
||||
expect(html).toContain('<pre');
|
||||
expect(html).toContain('plain code');
|
||||
});
|
||||
|
||||
test('should convert unordered lists', () => {
|
||||
const markdown = `- Item 1
|
||||
- Item 2
|
||||
- Item 3`;
|
||||
const html = markdownToHtml(markdown);
|
||||
|
||||
expect(html).toContain('<ul>');
|
||||
expect(html).toContain('<li>Item 1</li>');
|
||||
expect(html).toContain('<li>Item 2</li>');
|
||||
expect(html).toContain('<li>Item 3</li>');
|
||||
expect(html).toContain('</ul>');
|
||||
});
|
||||
|
||||
test('should convert ordered lists', () => {
|
||||
const markdown = `1. First
|
||||
2. Second
|
||||
3. Third`;
|
||||
const html = markdownToHtml(markdown);
|
||||
|
||||
expect(html).toContain('<ol>');
|
||||
expect(html).toContain('<li>First</li>');
|
||||
expect(html).toContain('<li>Second</li>');
|
||||
expect(html).toContain('<li>Third</li>');
|
||||
expect(html).toContain('</ol>');
|
||||
});
|
||||
|
||||
test('should convert links', () => {
|
||||
const markdown = '[Link Text](https://example.com)';
|
||||
const html = markdownToHtml(markdown);
|
||||
|
||||
expect(html).toContain('<a href="https://example.com">Link Text</a>');
|
||||
});
|
||||
|
||||
test('should convert images', () => {
|
||||
const markdown = '';
|
||||
const html = markdownToHtml(markdown);
|
||||
|
||||
expect(html).toContain('<img');
|
||||
expect(html).toContain('src="https://example.com/image.png"');
|
||||
expect(html).toContain('alt="Alt Text"');
|
||||
});
|
||||
|
||||
test('should convert blockquotes', () => {
|
||||
const markdown = '> This is a quote';
|
||||
const html = markdownToHtml(markdown);
|
||||
|
||||
expect(html).toContain('<blockquote>');
|
||||
expect(html).toContain('This is a quote');
|
||||
expect(html).toContain('</blockquote>');
|
||||
});
|
||||
|
||||
test('should convert tables', () => {
|
||||
const markdown = `| Header 1 | Header 2 |
|
||||
|----------|----------|
|
||||
| Cell 1 | Cell 2 |`;
|
||||
const html = markdownToHtml(markdown);
|
||||
|
||||
expect(html).toContain('<table>');
|
||||
expect(html).toContain('<thead>');
|
||||
expect(html).toContain('<tbody>');
|
||||
expect(html).toContain('<th>Header 1</th>');
|
||||
expect(html).toContain('<td>Cell 1</td>');
|
||||
});
|
||||
|
||||
test('should sanitize dangerous HTML (XSS protection)', () => {
|
||||
const markdown = '<script>alert("XSS")</script>';
|
||||
const html = markdownToHtml(markdown);
|
||||
|
||||
// Script tags should be removed
|
||||
expect(html).not.toContain('<script>');
|
||||
expect(html).not.toContain('alert');
|
||||
});
|
||||
|
||||
test('should sanitize dangerous onclick attributes', () => {
|
||||
const markdown = '<a href="#" onclick="alert(\'XSS\')">Click</a>';
|
||||
const html = markdownToHtml(markdown);
|
||||
|
||||
// onclick should be removed
|
||||
expect(html).not.toContain('onclick');
|
||||
});
|
||||
|
||||
test('should allow safe HTML attributes', () => {
|
||||
const markdown = '[Link](https://example.com "Title")';
|
||||
const html = markdownToHtml(markdown);
|
||||
|
||||
expect(html).toContain('href="https://example.com"');
|
||||
expect(html).toContain('title="Title"');
|
||||
});
|
||||
|
||||
test('should handle horizontal rules', () => {
|
||||
const markdown = '---';
|
||||
const html = markdownToHtml(markdown);
|
||||
|
||||
expect(html).toContain('<hr');
|
||||
});
|
||||
|
||||
test('should convert strikethrough (GFM)', () => {
|
||||
const markdown = '~~strikethrough~~';
|
||||
const html = markdownToHtml(markdown);
|
||||
|
||||
expect(html).toContain('<del>strikethrough</del>');
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractTOC', () => {
|
||||
test('should return empty array for null input', () => {
|
||||
expect(extractTOC(null)).toEqual([]);
|
||||
});
|
||||
|
||||
test('should return empty array for undefined input', () => {
|
||||
expect(extractTOC(undefined)).toEqual([]);
|
||||
});
|
||||
|
||||
test('should return empty array for empty string', () => {
|
||||
expect(extractTOC('')).toEqual([]);
|
||||
});
|
||||
|
||||
test('should return empty array for markdown without headings', () => {
|
||||
const markdown = 'Just a paragraph without headings.';
|
||||
expect(extractTOC(markdown)).toEqual([]);
|
||||
});
|
||||
|
||||
test('should extract single heading', () => {
|
||||
const markdown = '# Main Title';
|
||||
const toc = extractTOC(markdown);
|
||||
|
||||
expect(toc).toHaveLength(1);
|
||||
expect(toc[0]).toEqual({
|
||||
level: 1,
|
||||
title: 'Main Title',
|
||||
slug: 'main-title'
|
||||
});
|
||||
});
|
||||
|
||||
test('should extract multiple headings', () => {
|
||||
const markdown = `# Heading 1
|
||||
## Heading 2
|
||||
### Heading 3`;
|
||||
const toc = extractTOC(markdown);
|
||||
|
||||
expect(toc).toHaveLength(3);
|
||||
expect(toc[0].level).toBe(1);
|
||||
expect(toc[1].level).toBe(2);
|
||||
expect(toc[2].level).toBe(3);
|
||||
});
|
||||
|
||||
test('should extract headings with special characters', () => {
|
||||
const markdown = '# Test: Special Characters!';
|
||||
const toc = extractTOC(markdown);
|
||||
|
||||
expect(toc[0]).toEqual({
|
||||
level: 1,
|
||||
title: 'Test: Special Characters!',
|
||||
slug: 'test-special-characters'
|
||||
});
|
||||
});
|
||||
|
||||
test('should strip markdown formatting from titles', () => {
|
||||
const markdown = '# **Bold** and *Italic* and `code`';
|
||||
const toc = extractTOC(markdown);
|
||||
|
||||
expect(toc[0].title).toBe('Bold and Italic and code');
|
||||
});
|
||||
|
||||
test('should handle headings with multiple spaces', () => {
|
||||
const markdown = '# Multiple Spaces';
|
||||
const toc = extractTOC(markdown);
|
||||
|
||||
expect(toc[0].slug).toBe('multiple-spaces');
|
||||
});
|
||||
|
||||
test('should handle all heading levels (1-6)', () => {
|
||||
const markdown = `# H1
|
||||
## H2
|
||||
### H3
|
||||
#### H4
|
||||
##### H5
|
||||
###### H6`;
|
||||
const toc = extractTOC(markdown);
|
||||
|
||||
expect(toc).toHaveLength(6);
|
||||
expect(toc[0].level).toBe(1);
|
||||
expect(toc[5].level).toBe(6);
|
||||
});
|
||||
|
||||
test('should ignore invalid heading formats', () => {
|
||||
const markdown = `#No space
|
||||
# Valid Heading
|
||||
##No space
|
||||
## Another Valid`;
|
||||
const toc = extractTOC(markdown);
|
||||
|
||||
expect(toc).toHaveLength(2);
|
||||
expect(toc[0].title).toBe('Valid Heading');
|
||||
expect(toc[1].title).toBe('Another Valid');
|
||||
});
|
||||
|
||||
test('should handle headings mixed with content', () => {
|
||||
const markdown = `Some text
|
||||
# Heading 1
|
||||
More text
|
||||
## Heading 2
|
||||
Even more text`;
|
||||
const toc = extractTOC(markdown);
|
||||
|
||||
expect(toc).toHaveLength(2);
|
||||
expect(toc[0].title).toBe('Heading 1');
|
||||
expect(toc[1].title).toBe('Heading 2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractFrontMatter', () => {
|
||||
test('should return empty metadata for null input', () => {
|
||||
const result = extractFrontMatter(null);
|
||||
|
||||
expect(result).toEqual({
|
||||
metadata: {},
|
||||
content: null
|
||||
});
|
||||
});
|
||||
|
||||
test('should return empty metadata for undefined input', () => {
|
||||
const result = extractFrontMatter(undefined);
|
||||
|
||||
expect(result).toEqual({
|
||||
metadata: {},
|
||||
content: undefined
|
||||
});
|
||||
});
|
||||
|
||||
test('should return empty metadata for markdown without front matter', () => {
|
||||
const markdown = '# Just a heading';
|
||||
const result = extractFrontMatter(markdown);
|
||||
|
||||
expect(result.metadata).toEqual({});
|
||||
expect(result.content).toBe(markdown);
|
||||
});
|
||||
|
||||
test('should extract valid front matter', () => {
|
||||
const markdown = `---
|
||||
title: Test Document
|
||||
author: Test Author
|
||||
date: 2025-01-01
|
||||
---
|
||||
# Content starts here`;
|
||||
|
||||
const result = extractFrontMatter(markdown);
|
||||
|
||||
expect(result.metadata).toEqual({
|
||||
title: 'Test Document',
|
||||
author: 'Test Author',
|
||||
date: '2025-01-01'
|
||||
});
|
||||
expect(result.content).toBe('# Content starts here');
|
||||
});
|
||||
|
||||
test('should handle front matter with colons in values', () => {
|
||||
const markdown = `---
|
||||
url: https://example.com
|
||||
time: 12:30:45
|
||||
---
|
||||
Content`;
|
||||
|
||||
const result = extractFrontMatter(markdown);
|
||||
|
||||
expect(result.metadata.url).toBe('https://example.com');
|
||||
expect(result.metadata.time).toBe('12:30:45');
|
||||
});
|
||||
|
||||
test('should ignore lines without colons in front matter', () => {
|
||||
const markdown = `---
|
||||
title: Valid
|
||||
invalid line
|
||||
author: Also Valid
|
||||
---
|
||||
Content`;
|
||||
|
||||
const result = extractFrontMatter(markdown);
|
||||
|
||||
expect(result.metadata).toEqual({
|
||||
title: 'Valid',
|
||||
author: 'Also Valid'
|
||||
});
|
||||
});
|
||||
|
||||
test('should handle empty front matter block', () => {
|
||||
const markdown = `---
|
||||
---
|
||||
Content`;
|
||||
|
||||
const result = extractFrontMatter(markdown);
|
||||
|
||||
// Empty front matter doesn't match regex, returns original content
|
||||
expect(result.metadata).toEqual({});
|
||||
expect(result.content).toBe(markdown);
|
||||
});
|
||||
|
||||
test('should trim whitespace from keys and values', () => {
|
||||
const markdown = `---
|
||||
title : Trimmed Title
|
||||
author :Test Author
|
||||
---
|
||||
Content`;
|
||||
|
||||
const result = extractFrontMatter(markdown);
|
||||
|
||||
expect(result.metadata.title).toBe('Trimmed Title');
|
||||
expect(result.metadata.author).toBe('Test Author');
|
||||
});
|
||||
|
||||
test('should handle multiline content after front matter', () => {
|
||||
const markdown = `---
|
||||
title: Test
|
||||
---
|
||||
# Heading
|
||||
|
||||
Paragraph 1
|
||||
|
||||
Paragraph 2`;
|
||||
|
||||
const result = extractFrontMatter(markdown);
|
||||
|
||||
expect(result.metadata.title).toBe('Test');
|
||||
expect(result.content).toContain('# Heading');
|
||||
expect(result.content).toContain('Paragraph 1');
|
||||
expect(result.content).toContain('Paragraph 2');
|
||||
});
|
||||
|
||||
test('should handle front matter at end of document', () => {
|
||||
const markdown = `---
|
||||
title: Edge Case
|
||||
---`;
|
||||
|
||||
const result = extractFrontMatter(markdown);
|
||||
|
||||
// Regex requires content after closing ---, so this doesn't match
|
||||
expect(result.metadata).toEqual({});
|
||||
expect(result.content).toBe(markdown);
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateSlug', () => {
|
||||
test('should convert simple text to lowercase', () => {
|
||||
expect(generateSlug('Simple Text')).toBe('simple-text');
|
||||
});
|
||||
|
||||
test('should replace spaces with hyphens', () => {
|
||||
expect(generateSlug('Multiple Word Slug')).toBe('multiple-word-slug');
|
||||
});
|
||||
|
||||
test('should remove special characters', () => {
|
||||
expect(generateSlug('Special!@#$%Characters')).toBe('specialcharacters');
|
||||
});
|
||||
|
||||
test('should handle multiple spaces', () => {
|
||||
expect(generateSlug('Multiple Spaces Here')).toBe('multiple-spaces-here');
|
||||
});
|
||||
|
||||
test('should handle multiple hyphens', () => {
|
||||
expect(generateSlug('Multiple---Hyphens')).toBe('multiple-hyphens');
|
||||
});
|
||||
|
||||
test('should convert leading and trailing whitespace to hyphens', () => {
|
||||
// Note: trim() is called but only removes whitespace, not hyphens
|
||||
// Spaces are converted to hyphens before trim(), so leading/trailing spaces become hyphens
|
||||
expect(generateSlug(' Leading and Trailing ')).toBe('-leading-and-trailing-');
|
||||
});
|
||||
|
||||
test('should preserve leading and trailing hyphens', () => {
|
||||
// Hyphens are not trimmed, only whitespace
|
||||
expect(generateSlug('-Hyphen-Start-End-')).toBe('-hyphen-start-end-');
|
||||
});
|
||||
|
||||
test('should handle mixed case', () => {
|
||||
expect(generateSlug('MiXeD CaSe TeXt')).toBe('mixed-case-text');
|
||||
});
|
||||
|
||||
test('should handle numbers', () => {
|
||||
expect(generateSlug('Title 123 Numbers')).toBe('title-123-numbers');
|
||||
});
|
||||
|
||||
test('should handle underscores (keep them)', () => {
|
||||
expect(generateSlug('Text_With_Underscores')).toBe('text_with_underscores');
|
||||
});
|
||||
|
||||
test('should handle empty string', () => {
|
||||
expect(generateSlug('')).toBe('');
|
||||
});
|
||||
|
||||
test('should handle only special characters', () => {
|
||||
expect(generateSlug('!@#$%^&*()')).toBe('');
|
||||
});
|
||||
|
||||
test('should handle unicode characters', () => {
|
||||
expect(generateSlug('Café München')).toBe('caf-mnchen');
|
||||
});
|
||||
|
||||
test('should handle consecutive special characters', () => {
|
||||
expect(generateSlug('Word!!!Another???Word')).toBe('wordanotherword');
|
||||
});
|
||||
|
||||
test('should create valid URL slug', () => {
|
||||
const slug = generateSlug('What is the Tractatus Framework?');
|
||||
expect(slug).toBe('what-is-the-tractatus-framework');
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue