JIRA MCP Server

import { expect, test, describe, beforeEach, afterEach } from 'bun:test'; import { JiraApiService } from '../jira-api.js'; const mockFormattedDescription = { fields: { description: { content: [ { type: 'paragraph', content: [ { type: 'text', text: 'As a ' }, { type: 'text', text: 'user', marks: [{ type: 'em' }] }, { type: 'text', text: ' I want to see formatted text' } ] } ] } } }; describe('JiraApiService', () => { describe('cleanIssue', () => { test('should properly handle formatted text in description', () => { const service = new JiraApiService('http://test', 'test@test.com', 'token'); const result = (service as any).cleanIssue(mockFormattedDescription); expect(result.description).toBe('As a user I want to see formatted text'); }); }); const baseUrl = 'https://your-domain.atlassian.net'; const apiToken = 'test-token'; const email = 'user@domain.net'; let service: JiraApiService; let originalFetch: typeof fetch; beforeEach(() => { service = new JiraApiService(baseUrl, email, apiToken); originalFetch = global.fetch; }); afterEach(() => { global.fetch = originalFetch; }); describe('constructor', () => { test('should set up fetch with correct base URL and auth header', async () => { // Mock fetch to verify headers global.fetch = async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); expect(url.startsWith(baseUrl)).toBe(true); const headers = init?.headers as Headers; expect(headers.get('Authorization')).toBe(`Basic ${Buffer.from(`${email}:${apiToken}`).toString('base64')}`); expect(headers.get('Content-Type')).toBe('application/json'); return new Response(JSON.stringify({ issues: [] })); }; await service.searchIssues('project = TEST'); }); }); describe('searchIssues', () => { test('should make GET request to correct endpoint and clean response', async () => { const mockResponse = { issues: [ { id: '1', key: 'TEST-1', fields: { summary: 'Test Issue', status: { name: 'Open' }, created: '2024-01-01T00:00:00.000Z', updated: '2024-01-01T00:00:00.000Z', parent: { id: 'parent-1', key: 'TEST-PARENT', fields: { summary: 'Parent Issue' } }, subtasks: [ { id: 'child-1', key: 'TEST-CHILD', fields: { summary: 'Child Issue' } } ], customfield_10014: 'EPIC-1', description: { content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Test Description with mention of TEST-2' }, { type: 'inlineCard', attrs: { url: '/browse/TEST-3' } }] }] }, issuelinks: [{ type: { inward: 'is blocked by' }, inwardIssue: { key: 'TEST-4', fields: { summary: 'Blocking Issue' } } }] } } ], total: 1 }; const expectedResponse = { total: 1, issues: [{ id: '1', key: 'TEST-1', summary: 'Test Issue', description: 'Test Description with mention of TEST-2', status: 'Open', created: '2024-01-01T00:00:00.000Z', updated: '2024-01-01T00:00:00.000Z', parent: { id: 'parent-1', key: 'TEST-PARENT', summary: 'Parent Issue' }, children: [ { id: 'child-1', key: 'TEST-CHILD', summary: 'Child Issue' } ], epicLink: { id: 'EPIC-1', key: 'EPIC-1', summary: undefined }, relatedIssues: [ { key: 'TEST-2', type: 'mention' as const, source: 'description' as const }, { key: 'TEST-3', type: 'mention' as const, source: 'description' as const }, { key: 'TEST-4', summary: 'Blocking Issue', type: 'link' as const, relationship: 'is blocked by', source: 'description' as const } ] }] }; global.fetch = async () => new Response(JSON.stringify(mockResponse)); const result = await service.searchIssues('project = TEST'); expect(result).toEqual(expectedResponse); }); test('should handle error responses', async () => { global.fetch = async () => new Response( JSON.stringify({ message: 'You do not have permission' }), { status: 403 } ); await expect(service.searchIssues('project = TEST')).rejects.toThrow('JIRA API Error: You do not have permission'); }); }); describe('getEpicChildren', () => { const epicKey = 'TEST-1'; const mockResponse = { issues: [ { id: '2', key: 'TEST-2', fields: { summary: 'Child Issue', description: { content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Child Description' }] }] }, status: { name: 'Open' }, created: '2024-01-01T00:00:00.000Z', updated: '2024-01-01T00:00:00.000Z' } } ], total: 1 }; const mockComments = { comments: [ { id: '1', body: { content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Test Comment mentioning TEST-5' }, { type: 'inlineCard', attrs: { url: '/browse/TEST-6' } }] }] }, author: { displayName: 'Test User' }, created: '2024-01-01T00:00:00.000Z', updated: '2024-01-01T00:00:00.000Z' } ] }; test('should fetch epic children with comments', async () => { let fetchCount = 0; global.fetch = async (input: RequestInfo | URL) => { const url = input.toString(); if (url.includes('/search')) { return new Response(JSON.stringify(mockResponse)); } if (url.includes('/comment')) { return new Response(JSON.stringify(mockComments)); } throw new Error(`Unexpected URL: ${url}`); }; const expectedResponse = [{ id: '2', key: 'TEST-2', summary: 'Child Issue', description: 'Child Description', status: 'Open', created: '2024-01-01T00:00:00.000Z', updated: '2024-01-01T00:00:00.000Z', comments: [{ id: '1', body: 'Test Comment mentioning TEST-5', author: 'Test User', created: '2024-01-01T00:00:00.000Z', updated: '2024-01-01T00:00:00.000Z', mentions: [ { key: 'TEST-5', type: 'mention' as const, source: 'comment' as const, commentId: '1' }, { key: 'TEST-6', type: 'mention' as const, source: 'comment' as const, commentId: '1' } ] }], relatedIssues: [ { key: 'TEST-5', type: 'mention' as const, source: 'comment' as const, commentId: '1' }, { key: 'TEST-6', type: 'mention' as const, source: 'comment' as const, commentId: '1' } ] }]; const result = await service.getEpicChildren(epicKey); expect(result).toEqual(expectedResponse); }); test('should handle error responses', async () => { global.fetch = async () => new Response( JSON.stringify({ message: 'You do not have permission' }), { status: 403 } ); await expect(service.getEpicChildren(epicKey)).rejects.toThrow('JIRA API Error: You do not have permission'); }); }); describe('getIssueWithComments', () => { const issueId = 'TEST-1'; const mockIssue = { id: '1', key: 'TEST-1', fields: { summary: 'Test Issue', description: { content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Test Description with mention of TEST-7' }] }] }, status: { name: 'Open' }, created: '2024-01-01T00:00:00.000Z', updated: '2024-01-01T00:00:00.000Z', parent: { id: 'parent-1', key: 'TEST-PARENT', fields: { summary: 'Parent Issue' } }, subtasks: [ { id: 'child-1', key: 'TEST-CHILD', fields: { summary: 'Child Issue' } } ], customfield_10014: 'EPIC-1', issuelinks: [{ type: { outward: 'blocks' }, outwardIssue: { key: 'TEST-8', fields: { summary: 'Blocked Issue' } } }] } }; const mockEpic = { fields: { summary: 'Epic Issue' } }; const mockComments = { comments: [ { id: '1', body: { content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Test Comment mentioning TEST-9' }] }] }, author: { displayName: 'Test User' }, created: '2024-01-01T00:00:00.000Z', updated: '2024-01-01T00:00:00.000Z' } ] }; test('should make parallel requests for issue, comments, and epic details', async () => { global.fetch = async (input: RequestInfo | URL) => { const url = input.toString(); if (url.includes(`/issue/${issueId}?`)) { return new Response(JSON.stringify(mockIssue)); } if (url.includes('/comment')) { return new Response(JSON.stringify(mockComments)); } if (url.includes('/issue/EPIC-1')) { return new Response(JSON.stringify(mockEpic)); } throw new Error(`Unexpected URL: ${url}`); }; const result = await service.getIssueWithComments(issueId); expect(result).toEqual({ id: '1', key: 'TEST-1', summary: 'Test Issue', description: 'Test Description with mention of TEST-7', status: 'Open', created: '2024-01-01T00:00:00.000Z', updated: '2024-01-01T00:00:00.000Z', parent: { id: 'parent-1', key: 'TEST-PARENT', summary: 'Parent Issue' }, children: [ { id: 'child-1', key: 'TEST-CHILD', summary: 'Child Issue' } ], epicLink: { id: 'EPIC-1', key: 'EPIC-1', summary: 'Epic Issue' }, comments: [{ id: '1', body: 'Test Comment mentioning TEST-9', author: 'Test User', created: '2024-01-01T00:00:00.000Z', updated: '2024-01-01T00:00:00.000Z', mentions: [ { key: 'TEST-9', type: 'mention' as const, source: 'comment' as const, commentId: '1' } ] }], relatedIssues: [ { key: 'TEST-7', type: 'mention' as const, source: 'description' as const }, { key: 'TEST-8', summary: 'Blocked Issue', type: 'link' as const, relationship: 'blocks', source: 'description' as const }, { key: 'TEST-9', type: 'mention' as const, source: 'comment' as const, commentId: '1' } ] }); }); test('should handle epic fetch failure gracefully', async () => { global.fetch = async (input: RequestInfo | URL) => { const url = input.toString(); if (url.includes(`/issue/${issueId}?`)) { return new Response(JSON.stringify(mockIssue)); } if (url.includes('/comment')) { return new Response(JSON.stringify(mockComments)); } if (url.includes('/issue/EPIC-1')) { return new Response('Not Found', { status: 404 }); } throw new Error(`Unexpected URL: ${url}`); }; const result = await service.getIssueWithComments(issueId); expect(result.epicLink?.summary).toBeUndefined(); }); test('should handle 404 errors correctly', async () => { global.fetch = async () => new Response('Not Found', { status: 404 }); await expect(service.getIssueWithComments(issueId)).rejects.toThrow(`Issue not found: ${issueId}`); }); test('should handle permission errors', async () => { global.fetch = async () => new Response( JSON.stringify({ message: 'You do not have permission to view this issue' }), { status: 403 } ); await expect(service.getIssueWithComments(issueId)).rejects.toThrow( 'JIRA API Error: You do not have permission to view this issue' ); }); }); });