Skip to main content
Glama

mcp-jira-stdio

create-issue.test.ts18.4 kB
import { describe, it, expect, beforeEach, vi } from 'vitest'; import { handleCreateIssue, createIssueTool } from '../../../src/tools/create-issue.js'; import { validateInput } from '../../../src/utils/validators.js'; import { createIssue } from '../../../src/utils/api-helpers.js'; import { formatIssueResponse } from '../../../src/utils/formatters.js'; import { handleError } from '../../../src/utils/error-handler.js'; import { mockJiraIssue, mockJiraValidationErrorResponse } from '../../mocks/jira-responses.js'; import { TOOL_NAMES } from '../../../src/config/constants.js'; // Mock dependencies vi.mock('../../../src/utils/validators.js'); vi.mock('../../../src/utils/api-helpers.js'); vi.mock('../../../src/utils/formatters.js'); vi.mock('../../../src/utils/error-handler.js'); const mockedValidateInput = vi.mocked(validateInput); const mockedCreateIssue = vi.mocked(createIssue); const mockedFormatIssueResponse = vi.mocked(formatIssueResponse); const mockedHandleError = vi.mocked(handleError); describe('create-issue tool', () => { beforeEach(() => { vi.clearAllMocks(); }); describe('createIssueTool configuration', () => { it('should have correct tool configuration', () => { expect(createIssueTool.name).toBe(TOOL_NAMES.CREATE_ISSUE); expect(createIssueTool.description).toContain('Creates a new Jira issue'); expect(createIssueTool.inputSchema.type).toBe('object'); expect(createIssueTool.inputSchema.required).toEqual(['projectKey', 'summary', 'issueType']); // Check required fields expect(createIssueTool.inputSchema.properties.projectKey).toBeDefined(); expect(createIssueTool.inputSchema.properties.summary).toBeDefined(); expect(createIssueTool.inputSchema.properties.issueType).toBeDefined(); // Check optional fields expect(createIssueTool.inputSchema.properties.description).toBeDefined(); expect(createIssueTool.inputSchema.properties.priority).toBeDefined(); expect(createIssueTool.inputSchema.properties.assignee).toBeDefined(); expect(createIssueTool.inputSchema.properties.labels).toBeDefined(); expect(createIssueTool.inputSchema.properties.components).toBeDefined(); }); }); describe('handleCreateIssue', () => { describe('Success Cases', () => { it('should create issue with required fields only', async () => { const input = { projectKey: 'TEST', summary: 'New bug report', issueType: 'Bug', }; const validatedInput = { ...input }; const mockResponse = { content: [{ type: 'text', text: 'formatted issue' }] }; mockedValidateInput.mockReturnValue(validatedInput); mockedCreateIssue.mockResolvedValue(mockJiraIssue); mockedFormatIssueResponse.mockReturnValue(mockResponse); const result = await handleCreateIssue(input); expect(mockedValidateInput).toHaveBeenCalledWith(expect.any(Object), input); expect(mockedCreateIssue).toHaveBeenCalledWith({ projectKey: 'TEST', summary: 'New bug report', issueType: 'Bug', }); expect(mockedFormatIssueResponse).toHaveBeenCalledWith(mockJiraIssue); expect(result).toEqual(mockResponse); }); it('should create issue with all optional fields', async () => { const input = { projectKey: 'TEST', summary: 'New bug report', description: 'Detailed description', issueType: 'Bug', priority: 'High', assignee: 'user-123', labels: ['urgent', 'bug'], components: ['Frontend', 'Backend'], }; const validatedInput = { ...input }; mockedValidateInput.mockReturnValue(validatedInput); mockedCreateIssue.mockResolvedValue(mockJiraIssue); mockedFormatIssueResponse.mockReturnValue({ content: [] }); await handleCreateIssue(input); expect(mockedCreateIssue).toHaveBeenCalledWith({ projectKey: 'TEST', summary: 'New bug report', description: 'Detailed description', issueType: 'Bug', priority: 'High', assignee: 'user-123', labels: ['urgent', 'bug'], components: ['Frontend', 'Backend'], }); }); it('should handle partial optional fields', async () => { const input = { projectKey: 'TEST', summary: 'New bug report', issueType: 'Bug', priority: 'High', labels: ['bug'], }; const validatedInput = { ...input }; mockedValidateInput.mockReturnValue(validatedInput); mockedCreateIssue.mockResolvedValue(mockJiraIssue); mockedFormatIssueResponse.mockReturnValue({ content: [] }); await handleCreateIssue(input); expect(mockedCreateIssue).toHaveBeenCalledWith({ projectKey: 'TEST', summary: 'New bug report', issueType: 'Bug', priority: 'High', labels: ['bug'], }); }); it('should forward customFields to API helper', async () => { const input = { projectKey: 'TEST', summary: 'New task', issueType: 'Task', customFields: { customfield_10071: { id: '20010' }, }, } as any; const validatedInput = { ...input }; mockedValidateInput.mockReturnValue(validatedInput); mockedCreateIssue.mockResolvedValue(mockJiraIssue); mockedFormatIssueResponse.mockReturnValue({ content: [] }); await handleCreateIssue(input); expect(mockedCreateIssue).toHaveBeenCalledWith({ projectKey: 'TEST', summary: 'New task', issueType: 'Task', customFields: { customfield_10071: { id: '20010' } }, }); }); it('should handle empty arrays for labels and components', async () => { const input = { projectKey: 'TEST', summary: 'New bug report', issueType: 'Bug', labels: [], components: [], }; const validatedInput = { ...input }; mockedValidateInput.mockReturnValue(validatedInput); mockedCreateIssue.mockResolvedValue(mockJiraIssue); mockedFormatIssueResponse.mockReturnValue({ content: [] }); await handleCreateIssue(input); expect(mockedCreateIssue).toHaveBeenCalledWith({ projectKey: 'TEST', summary: 'New bug report', issueType: 'Bug', labels: [], components: [], }); }); it('should handle undefined optional fields gracefully', async () => { const validatedInput = { projectKey: 'TEST', summary: 'New bug report', issueType: 'Bug', description: undefined, priority: undefined, assignee: undefined, labels: undefined, components: undefined, }; mockedValidateInput.mockReturnValue(validatedInput); mockedCreateIssue.mockResolvedValue(mockJiraIssue); mockedFormatIssueResponse.mockReturnValue({ content: [] }); await handleCreateIssue({}); const expectedParams = { projectKey: 'TEST', summary: 'New bug report', issueType: 'Bug', }; expect(mockedCreateIssue).toHaveBeenCalledWith(expectedParams); }); }); describe('Validation', () => { it('should validate input using schema', async () => { const input = { projectKey: 'TEST', summary: 'New bug report', issueType: 'Bug', }; mockedValidateInput.mockReturnValue(input); mockedCreateIssue.mockResolvedValue(mockJiraIssue); mockedFormatIssueResponse.mockReturnValue({ content: [] }); await handleCreateIssue(input); expect(mockedValidateInput).toHaveBeenCalledWith( expect.objectContaining({ _def: expect.objectContaining({ typeName: 'ZodObject', }), }), input ); }); it('should handle validation errors for missing required fields', async () => { const input = { projectKey: 'TEST' }; // missing summary and issueType const validationError = new Error( 'Validation failed: summary is required, issueType is required' ); const mockErrorResponse = { content: [{ type: 'text', text: 'Validation error' }] }; mockedValidateInput.mockImplementation(() => { throw validationError; }); mockedHandleError.mockReturnValue(mockErrorResponse); const result = await handleCreateIssue(input); expect(mockedHandleError).toHaveBeenCalledWith(validationError); expect(result).toEqual(mockErrorResponse); }); it('should handle validation errors for invalid field types', async () => { const input = { projectKey: 'TEST', summary: 123, // should be string issueType: 'Bug', }; const validationError = new Error('Validation failed: summary must be string'); const mockErrorResponse = { content: [{ type: 'text', text: 'Validation error' }] }; mockedValidateInput.mockImplementation(() => { throw validationError; }); mockedHandleError.mockReturnValue(mockErrorResponse); const result = await handleCreateIssue(input); expect(mockedHandleError).toHaveBeenCalledWith(validationError); expect(result).toEqual(mockErrorResponse); }); it('should handle validation errors for empty summary', async () => { const input = { projectKey: 'TEST', summary: '', // empty string issueType: 'Bug', }; const validationError = new Error( 'Validation failed: summary must be at least 1 character' ); const mockErrorResponse = { content: [{ type: 'text', text: 'Validation error' }] }; mockedValidateInput.mockImplementation(() => { throw validationError; }); mockedHandleError.mockReturnValue(mockErrorResponse); const result = await handleCreateIssue(input); expect(mockedHandleError).toHaveBeenCalledWith(validationError); expect(result).toEqual(mockErrorResponse); }); }); describe('Error Handling', () => { it('should handle API errors from createIssue', async () => { const input = { projectKey: 'TEST', summary: 'New bug report', issueType: 'Bug', }; const apiError = { response: { status: 400, data: mockJiraValidationErrorResponse, }, }; const mockErrorResponse = { content: [{ type: 'text', text: 'API error' }] }; mockedValidateInput.mockReturnValue(input); mockedCreateIssue.mockRejectedValue(apiError); mockedHandleError.mockReturnValue(mockErrorResponse); const result = await handleCreateIssue(input); expect(mockedHandleError).toHaveBeenCalledWith(apiError); expect(result).toEqual(mockErrorResponse); }); it('should handle authentication errors', async () => { const input = { projectKey: 'TEST', summary: 'New bug report', issueType: 'Bug', }; const authError = { response: { status: 401, data: { errorMessages: ['Unauthorized'] } } }; const mockErrorResponse = { content: [{ type: 'text', text: 'Auth error' }] }; mockedValidateInput.mockReturnValue(input); mockedCreateIssue.mockRejectedValue(authError); mockedHandleError.mockReturnValue(mockErrorResponse); const result = await handleCreateIssue(input); expect(mockedHandleError).toHaveBeenCalledWith(authError); expect(result).toEqual(mockErrorResponse); }); it('should handle permission errors', async () => { const input = { projectKey: 'TEST', summary: 'New bug report', issueType: 'Bug', }; const permissionError = { response: { status: 403, data: { errorMessages: ['Insufficient permissions to create issues'] }, }, }; const mockErrorResponse = { content: [{ type: 'text', text: 'Permission error' }] }; mockedValidateInput.mockReturnValue(input); mockedCreateIssue.mockRejectedValue(permissionError); mockedHandleError.mockReturnValue(mockErrorResponse); const result = await handleCreateIssue(input); expect(mockedHandleError).toHaveBeenCalledWith(permissionError); expect(result).toEqual(mockErrorResponse); }); it('should handle project not found errors', async () => { const input = { projectKey: 'NONEXISTENT', summary: 'New bug report', issueType: 'Bug', }; const notFoundError = { response: { status: 404, data: { errorMessages: ['Project not found'] }, }, }; const mockErrorResponse = { content: [{ type: 'text', text: 'Not found error' }] }; mockedValidateInput.mockReturnValue(input); mockedCreateIssue.mockRejectedValue(notFoundError); mockedHandleError.mockReturnValue(mockErrorResponse); const result = await handleCreateIssue(input); expect(mockedHandleError).toHaveBeenCalledWith(notFoundError); expect(result).toEqual(mockErrorResponse); }); it('should handle network errors', async () => { const input = { projectKey: 'TEST', summary: 'New bug report', issueType: 'Bug', }; const networkError = new Error('Network Error'); networkError.code = 'ECONNREFUSED'; const mockErrorResponse = { content: [{ type: 'text', text: 'Network error' }] }; mockedValidateInput.mockReturnValue(input); mockedCreateIssue.mockRejectedValue(networkError); mockedHandleError.mockReturnValue(mockErrorResponse); const result = await handleCreateIssue(input); expect(mockedHandleError).toHaveBeenCalledWith(networkError); expect(result).toEqual(mockErrorResponse); }); it('should handle rate limit errors', async () => { const input = { projectKey: 'TEST', summary: 'New bug report', issueType: 'Bug', }; const rateLimitError = { response: { status: 429, data: { errorMessages: ['Rate limit exceeded'] }, }, }; const mockErrorResponse = { content: [{ type: 'text', text: 'Rate limit error' }] }; mockedValidateInput.mockReturnValue(input); mockedCreateIssue.mockRejectedValue(rateLimitError); mockedHandleError.mockReturnValue(mockErrorResponse); const result = await handleCreateIssue(input); expect(mockedHandleError).toHaveBeenCalledWith(rateLimitError); expect(result).toEqual(mockErrorResponse); }); }); describe('Edge Cases', () => { it('should handle very long summary', async () => { const longSummary = 'A'.repeat(1000); const input = { projectKey: 'TEST', summary: longSummary, issueType: 'Bug', }; mockedValidateInput.mockReturnValue(input); mockedCreateIssue.mockResolvedValue(mockJiraIssue); mockedFormatIssueResponse.mockReturnValue({ content: [] }); await handleCreateIssue(input); expect(mockedCreateIssue).toHaveBeenCalledWith({ projectKey: 'TEST', summary: longSummary, issueType: 'Bug', }); }); it('should handle special characters in fields', async () => { const input = { projectKey: 'TEST', summary: 'Bug with special chars: äöü@#$%&', description: 'Description with\nnewlines\tand\ttabs', issueType: 'Bug', }; mockedValidateInput.mockReturnValue(input); mockedCreateIssue.mockResolvedValue(mockJiraIssue); mockedFormatIssueResponse.mockReturnValue({ content: [] }); await handleCreateIssue(input); expect(mockedCreateIssue).toHaveBeenCalledWith({ projectKey: 'TEST', summary: 'Bug with special chars: äöü@#$%&', description: 'Description with\nnewlines\tand\ttabs', issueType: 'Bug', }); }); it('should handle large arrays of labels and components', async () => { const input = { projectKey: 'TEST', summary: 'Bug with many labels', issueType: 'Bug', labels: Array(50) .fill('label') .map((l, i) => `${l}-${i}`), components: Array(20) .fill('component') .map((c, i) => `${c}-${i}`), }; mockedValidateInput.mockReturnValue(input); mockedCreateIssue.mockResolvedValue(mockJiraIssue); mockedFormatIssueResponse.mockReturnValue({ content: [] }); await handleCreateIssue(input); expect(mockedCreateIssue).toHaveBeenCalledWith(input); }); it('should handle null input', async () => { const validationError = new Error('Input cannot be null'); const mockErrorResponse = { content: [{ type: 'text', text: 'Validation error' }] }; mockedValidateInput.mockImplementation(() => { throw validationError; }); mockedHandleError.mockReturnValue(mockErrorResponse); const result = await handleCreateIssue(null); expect(result).toEqual(mockErrorResponse); }); it('should handle empty input object', async () => { const input = {}; const validationError = new Error('Required fields missing'); const mockErrorResponse = { content: [{ type: 'text', text: 'Validation error' }] }; mockedValidateInput.mockImplementation(() => { throw validationError; }); mockedHandleError.mockReturnValue(mockErrorResponse); const result = await handleCreateIssue(input); expect(result).toEqual(mockErrorResponse); }); }); }); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/freema/mcp-jira-stdio'

If you have feedback or need assistance with the MCP directory API, please join our Discord server