lamplighter-mcp
Version:
An intelligent context engine for AI-assisted software development
171 lines (143 loc) • 6.86 kB
text/typescript
import axios from 'axios';
import { ConfluenceReader } from '../../src/modules/confluenceReader';
import dotenv from 'dotenv';
// Ensure dotenv is configured for tests if needed, or mock process.env directly
dotenv.config();
// Mock axios
jest.mock('axios');
const mockedAxios = jest.mocked(axios);
// Save original environment variables
const originalEnv = process.env;
describe('ConfluenceReader', () => {
beforeEach(() => {
// Reset mocks and environment variables before each test
jest.clearAllMocks();
process.env = {
...originalEnv,
CONFLUENCE_URL: 'https://example.atlassian.net/wiki',
CONFLUENCE_USERNAME: 'testuser@example.com',
CONFLUENCE_API_TOKEN: 'testtoken'
};
});
afterAll(() => {
// Restore original environment variables
process.env = originalEnv;
});
it('should throw error if CONFLUENCE_URL is not set', () => {
delete process.env.CONFLUENCE_URL;
expect(() => new ConfluenceReader()).toThrow('Confluence URL is required');
});
it('should throw error if CONFLUENCE_USERNAME is not set', () => {
delete process.env.CONFLUENCE_USERNAME;
expect(() => new ConfluenceReader()).toThrow('Confluence username is required');
});
it('should throw error if CONFLUENCE_API_TOKEN is not set', () => {
delete process.env.CONFLUENCE_API_TOKEN;
expect(() => new ConfluenceReader()).toThrow('Confluence API token is required');
});
describe('extractPageId', () => {
let reader: ConfluenceReader;
beforeEach(() => {
reader = new ConfluenceReader();
// Access private method for testing (TypeScript workaround)
reader['extractPageId'] = reader['extractPageId'].bind(reader);
});
it.each([
['https://example.atlassian.net/wiki/spaces/SPACE/pages/123456/Page+Title', '123456'],
['https://example.atlassian.net/wiki/spaces/SPACE/pages/98765', '98765'],
['https://example.atlassian.net/wiki/pages/view/54321', '54321'],
['https://example.atlassian.net/wiki/pages?pageId=67890', '67890'],
['112233', '112233'] // Direct ID
])('should extract page ID %s correctly', (urlOrId, expectedId) => {
expect(reader['extractPageId'](urlOrId)).toBe(expectedId);
});
it('should throw error for invalid URL/ID', () => {
expect(() => reader['extractPageId']('invalid-url')).toThrow('Invalid Confluence URL format: invalid-url');
expect(() => reader['extractPageId']('https://example.com/no/id/here')).toThrow('Could not extract page ID from URL: https://example.com/no/id/here');
});
});
describe('fetchPageContent', () => {
let reader: ConfluenceReader;
const pageId = '123456';
const pageUrl = `https://example.atlassian.net/wiki/spaces/SPACE/pages/${pageId}/Page+Title`;
const apiUrl = `https://example.atlassian.net/wiki/rest/api/content/${pageId}?expand=body.storage`;
const expectedAuth = `Basic ${Buffer.from('testuser@example.com:testtoken').toString('base64')}`;
beforeEach(() => {
reader = new ConfluenceReader();
});
it('should fetch and extract content successfully', async () => {
const mockHtml = '<p>Hello <b>World</b>!"</p><br/><p>Test.</p>';
const expectedText = 'Hello World!"\n\nTest.';
mockedAxios.get.mockResolvedValue({
data: {
title: 'Test Page',
body: {
storage: {
value: mockHtml
}
}
}
});
const content = await reader.fetchPageContent(pageUrl);
expect(mockedAxios.get).toHaveBeenCalledTimes(1);
expect(mockedAxios.get).toHaveBeenCalledWith(apiUrl, {
headers: {
'Authorization': expectedAuth,
'Content-Type': 'application/json'
}
});
expect(content).toBe(expectedText);
});
// it('should handle Confluence API error (e.g., 404)', async () => {
// const errorResponse = {
// response: {
// status: 404,
// data: { message: 'Page not found' }
// }
// };
// const axiosError = Object.assign(new Error('Request failed with status code 404'), errorResponse, { isAxiosError: true, config: {}, request: {} });
// mockedAxios.get.mockRejectedValue(axiosError);
// await expect(reader.fetchPageContent(pageUrl))
// .rejects.toThrow('Confluence API error: 404 - Page not found');
// expect(mockedAxios.get).toHaveBeenCalledTimes(1);
// expect(mockedAxios.get).toHaveBeenCalledWith(apiUrl, expect.any(Object));
// });
// it('should handle network error', async () => {
// const errorRequest = { request: {} }; // Simulate request made but no response
// const networkError = Object.assign(new Error('Network Error'), errorRequest, { isAxiosError: true, config: {} });
// mockedAxios.get.mockRejectedValue(networkError);
// await expect(reader.fetchPageContent(pageUrl))
// .rejects.toThrow('No response received from Confluence. Please check your network connection and Confluence URL.');
// expect(mockedAxios.get).toHaveBeenCalledTimes(1);
// });
it('should handle generic error', async () => {
const genericError = new Error('Something else failed');
mockedAxios.get.mockRejectedValue(genericError);
await expect(reader.fetchPageContent(pageUrl)).rejects.toThrow(`Failed to fetch Confluence page: ${genericError.message}`);
expect(mockedAxios.get).toHaveBeenCalledTimes(1);
});
});
describe('extractTextFromHtml', () => {
let reader: ConfluenceReader;
beforeEach(() => {
reader = new ConfluenceReader();
// Access private method for testing (TypeScript workaround)
reader['extractTextFromHtml'] = reader['extractTextFromHtml'].bind(reader);
});
it('should extract text from basic HTML', () => {
const html = '<p>Line 1.</p> <p>Line <b>2</b> with & entities and<br/>break.</p><div>Div line</div>';
const expected = 'Line 1.\n\nLine 2 with & entities and\nbreak.\n\nDiv line';
expect(reader['extractTextFromHtml'](html)).toBe(expected);
});
it('should handle empty or only whitespace HTML', () => {
expect(reader['extractTextFromHtml'](null as any)).toBe('');
expect(reader['extractTextFromHtml']('')).toBe('');
expect(reader['extractTextFromHtml'](' <p> </p> ')).toBe('');
});
it('should strip complex tags and handle spacing', () => {
const html = '<script>alert("bad")</script><div><a href="#">Link</a> Text<span> More</span></div><p>Paragraph</p>';
const expected = 'Link Text More\n\nParagraph';
expect(reader['extractTextFromHtml'](html)).toBe(expected);
});
});
});