Skip to main content
Glama

mcp-jira-stdio

api-helpers.test.ts•23.2 kB
import { describe, it, expect, beforeEach, vi } from 'vitest'; import { makeJiraRequest } from '../../../src/utils/jira-auth.js'; import { getVisibleProjects, getIssue, searchIssues, createIssue, updateIssue, getCurrentUser, getMyIssues, getIssueTypes, getUsers, getPriorities, getStatuses, addComment, getProjectDetails, createSubtask, } from '../../../src/utils/api-helpers.js'; import { mockJiraProject, mockJiraIssue, mockJiraSearchResult, mockJiraUser, mockJiraIssueType, mockJiraSubtaskIssueType, mockJiraPriority, mockJiraStatus, mockJiraComment, mockJiraProjectDetails, mockJiraPaginatedProjects, mockJiraCreateIssueResponse, } from '../../mocks/jira-responses.js'; // Mock the jira-auth module vi.mock('../../../src/utils/jira-auth.js'); const mockedMakeJiraRequest = vi.mocked(makeJiraRequest); describe('api-helpers', () => { beforeEach(() => { vi.clearAllMocks(); }); describe('getVisibleProjects', () => { it('should fetch projects without options', async () => { mockedMakeJiraRequest.mockResolvedValue(mockJiraPaginatedProjects); const result = await getVisibleProjects(); expect(mockedMakeJiraRequest).toHaveBeenCalledWith({ method: 'GET', url: '/project/search', params: {}, }); expect(result).toEqual([mockJiraProject]); }); it('should fetch projects with expand options', async () => { mockedMakeJiraRequest.mockResolvedValue(mockJiraPaginatedProjects); await getVisibleProjects({ expand: ['lead', 'description'] }); expect(mockedMakeJiraRequest).toHaveBeenCalledWith({ method: 'GET', url: '/project/search', params: { expand: 'lead,description' }, }); }); it('should fetch recent projects', async () => { mockedMakeJiraRequest.mockResolvedValue(mockJiraPaginatedProjects); await getVisibleProjects({ recent: 5 }); expect(mockedMakeJiraRequest).toHaveBeenCalledWith({ method: 'GET', url: '/project/search', params: { recent: 5 }, }); }); it('should fetch projects with both expand and recent options', async () => { mockedMakeJiraRequest.mockResolvedValue(mockJiraPaginatedProjects); await getVisibleProjects({ expand: ['lead'], recent: 3 }); expect(mockedMakeJiraRequest).toHaveBeenCalledWith({ method: 'GET', url: '/project/search', params: { expand: 'lead', recent: 3 }, }); }); }); describe('getIssue', () => { it('should fetch issue without options', async () => { mockedMakeJiraRequest.mockResolvedValue(mockJiraIssue); const result = await getIssue('TEST-123'); expect(mockedMakeJiraRequest).toHaveBeenCalledWith({ method: 'GET', url: '/issue/TEST-123', params: {}, }); expect(result).toEqual(mockJiraIssue); }); it('should fetch issue with expand options', async () => { mockedMakeJiraRequest.mockResolvedValue(mockJiraIssue); await getIssue('TEST-123', { expand: ['comments', 'attachments'] }); expect(mockedMakeJiraRequest).toHaveBeenCalledWith({ method: 'GET', url: '/issue/TEST-123', params: { expand: 'comments,attachments' }, }); }); it('should fetch issue with specific fields', async () => { mockedMakeJiraRequest.mockResolvedValue(mockJiraIssue); await getIssue('TEST-123', { fields: ['summary', 'status'] }); expect(mockedMakeJiraRequest).toHaveBeenCalledWith({ method: 'GET', url: '/issue/TEST-123', params: { fields: 'summary,status' }, }); }); it('should fetch issue with both expand and fields', async () => { mockedMakeJiraRequest.mockResolvedValue(mockJiraIssue); await getIssue('TEST-123', { expand: ['comments'], fields: ['summary', 'status'], }); expect(mockedMakeJiraRequest).toHaveBeenCalledWith({ method: 'GET', url: '/issue/TEST-123', params: { expand: 'comments', fields: 'summary,status', }, }); }); }); describe('searchIssues', () => { it('should search issues with basic JQL', async () => { mockedMakeJiraRequest.mockResolvedValue(mockJiraSearchResult); const result = await searchIssues({ jql: 'project = TEST' }); expect(mockedMakeJiraRequest).toHaveBeenCalledWith({ method: 'POST', url: '/search/jql', data: { jql: 'project = TEST', maxResults: 50, }, }); expect(result).toEqual(mockJiraSearchResult); }); it('should search issues with pagination token', async () => { mockedMakeJiraRequest.mockResolvedValue(mockJiraSearchResult); await searchIssues({ jql: 'project = TEST', nextPageToken: 'token-abc-123', maxResults: 10, }); expect(mockedMakeJiraRequest).toHaveBeenCalledWith({ method: 'POST', url: '/search/jql', data: { jql: 'project = TEST', nextPageToken: 'token-abc-123', maxResults: 10, }, }); }); it('should search issues with fields and expand', async () => { mockedMakeJiraRequest.mockResolvedValue(mockJiraSearchResult); await searchIssues({ jql: 'project = TEST', fields: ['summary', 'status'], expand: ['changelog'], }); expect(mockedMakeJiraRequest).toHaveBeenCalledWith({ method: 'POST', url: '/search/jql', data: { jql: 'project = TEST', maxResults: 50, fields: ['summary', 'status'], expand: ['changelog'], }, }); }); it('should sanitize dangerous JQL characters', async () => { mockedMakeJiraRequest.mockResolvedValue(mockJiraSearchResult); await searchIssues({ jql: 'summary ~ "test<script>"' }); expect(mockedMakeJiraRequest).toHaveBeenCalledWith({ method: 'POST', url: '/search/jql', data: { jql: 'summary ~ testscript', maxResults: 50, }, }); }); }); describe('createIssue', () => { it('should create issue with required fields only', async () => { mockedMakeJiraRequest .mockResolvedValueOnce(mockJiraCreateIssueResponse) .mockResolvedValueOnce(mockJiraIssue); const issueData = { projectKey: 'TEST', summary: 'Test issue', issueType: 'Bug', }; const result = await createIssue(issueData); expect(mockedMakeJiraRequest).toHaveBeenNthCalledWith(1, { method: 'POST', url: '/issue', data: { fields: { project: { key: 'TEST' }, summary: 'Test issue', issuetype: { name: 'Bug' }, }, }, }); expect(mockedMakeJiraRequest).toHaveBeenNthCalledWith(2, { method: 'GET', url: '/issue/TEST-123', params: {}, }); expect(result).toEqual(mockJiraIssue); }); it('should create issue with all optional fields', async () => { mockedMakeJiraRequest .mockResolvedValueOnce(mockJiraCreateIssueResponse) .mockResolvedValueOnce(mockJiraIssue); const issueData = { projectKey: 'TEST', summary: 'Test issue', description: 'Test description', issueType: 'Bug', priority: 'High', assignee: 'user-id-123', labels: ['bug', 'urgent'], components: ['Frontend', 'Backend'], }; await createIssue(issueData); const firstCall = mockedMakeJiraRequest.mock.calls[0][0]; expect(firstCall).toMatchObject({ method: 'POST', url: '/issue', }); expect(firstCall.data.fields).toMatchObject({ project: { key: 'TEST' }, summary: 'Test issue', issuetype: { name: 'Bug' }, priority: { name: 'High' }, assignee: { accountId: 'user-id-123' }, labels: ['bug', 'urgent'], components: [{ name: 'Frontend' }, { name: 'Backend' }], }); expect(firstCall.data.fields.description).toMatchObject({ type: 'doc', version: 1 }); }); it('should handle empty arrays for labels and components', async () => { mockedMakeJiraRequest .mockResolvedValueOnce(mockJiraCreateIssueResponse) .mockResolvedValueOnce(mockJiraIssue); const issueData = { projectKey: 'TEST', summary: 'Test issue', issueType: 'Bug', labels: [], components: [], }; await createIssue(issueData); const callData = mockedMakeJiraRequest.mock.calls[0][0].data; expect(callData.fields.labels).toBeUndefined(); expect(callData.fields.components).toBeUndefined(); }); it('should pass through customFields into payload', async () => { mockedMakeJiraRequest .mockResolvedValueOnce(mockJiraCreateIssueResponse) .mockResolvedValueOnce(mockJiraIssue); const issueData = { projectKey: 'TEST', summary: 'Test issue', issueType: 'Task', customFields: { customfield_10071: { id: '20010' }, customfield_12345: 'some value', }, } as any; await createIssue(issueData); const firstCall = mockedMakeJiraRequest.mock.calls[0][0]; expect(firstCall).toMatchObject({ method: 'POST', url: '/issue' }); expect(firstCall.data.fields.customfield_10071).toEqual({ id: '20010' }); expect(firstCall.data.fields.customfield_12345).toEqual('some value'); // Ensure standard fields preserved expect(firstCall.data.fields.summary).toBe('Test issue'); expect(firstCall.data.fields.issuetype).toEqual({ name: 'Task' }); }); }); describe('updateIssue', () => { it('should update issue with provided fields', async () => { mockedMakeJiraRequest.mockResolvedValue(undefined); const updates = { summary: 'Updated summary', description: 'Updated description', priority: 'Low', }; await updateIssue('TEST-123', updates); const call = mockedMakeJiraRequest.mock.calls[0][0]; expect(call).toMatchObject({ method: 'PUT', url: '/issue/TEST-123' }); expect(call.data.fields.summary).toBe('Updated summary'); expect(call.data.fields.priority).toEqual({ name: 'Low' }); expect(call.data.fields.description).toMatchObject({ type: 'doc', version: 1 }); }); it('should handle assignee updates', async () => { mockedMakeJiraRequest.mockResolvedValue(undefined); await updateIssue('TEST-123', { assignee: 'new-user-id' }); expect(mockedMakeJiraRequest).toHaveBeenCalledWith({ method: 'PUT', url: '/issue/TEST-123', data: { fields: { assignee: { accountId: 'new-user-id' }, }, }, }); }); it('should handle assignee removal', async () => { mockedMakeJiraRequest.mockResolvedValue(undefined); await updateIssue('TEST-123', { assignee: '' }); expect(mockedMakeJiraRequest).toHaveBeenCalledWith({ method: 'PUT', url: '/issue/TEST-123', data: { fields: { assignee: null, }, }, }); }); it('should handle labels and components updates', async () => { mockedMakeJiraRequest.mockResolvedValue(undefined); await updateIssue('TEST-123', { labels: ['new-label'], components: ['New Component'], }); expect(mockedMakeJiraRequest).toHaveBeenCalledWith({ method: 'PUT', url: '/issue/TEST-123', data: { fields: { labels: ['new-label'], components: [{ name: 'New Component' }], }, }, }); }); it('should only include defined fields in update', async () => { mockedMakeJiraRequest.mockResolvedValue(undefined); await updateIssue('TEST-123', { summary: 'New summary' }); const callData = mockedMakeJiraRequest.mock.calls[0][0].data; expect(callData.fields).toEqual({ summary: 'New summary', }); expect(callData.fields.description).toBeUndefined(); expect(callData.fields.priority).toBeUndefined(); }); }); describe('getCurrentUser', () => { it('should fetch current user', async () => { mockedMakeJiraRequest.mockResolvedValue(mockJiraUser); const result = await getCurrentUser(); expect(mockedMakeJiraRequest).toHaveBeenCalledWith({ method: 'GET', url: '/myself', }); expect(result).toEqual(mockJiraUser); }); }); describe('getMyIssues', () => { it("should fetch current user's issues using currentUser() JQL", async () => { mockedMakeJiraRequest.mockResolvedValueOnce(mockJiraSearchResult); const result = await getMyIssues(); expect(mockedMakeJiraRequest).toHaveBeenCalledWith({ method: 'POST', url: '/search/jql', data: { jql: 'assignee = currentUser() ORDER BY updated DESC', maxResults: 50, }, }); expect(result).toEqual(mockJiraSearchResult); }); it('should fetch my issues with options', async () => { mockedMakeJiraRequest.mockResolvedValueOnce(mockJiraSearchResult); await getMyIssues({ nextPageToken: 'token-def-456', maxResults: 25, fields: ['summary'], expand: ['comments'], }); expect(mockedMakeJiraRequest).toHaveBeenCalledWith({ method: 'POST', url: '/search/jql', data: { jql: 'assignee = currentUser() ORDER BY updated DESC', nextPageToken: 'token-def-456', maxResults: 25, fields: ['summary'], expand: ['comments'], }, }); }); }); describe('getIssueTypes', () => { it('should fetch global issue types', async () => { const issueTypes = [mockJiraIssueType]; mockedMakeJiraRequest.mockResolvedValue(issueTypes); const result = await getIssueTypes(); expect(mockedMakeJiraRequest).toHaveBeenCalledWith({ method: 'GET', url: '/issuetype', }); expect(result).toEqual(issueTypes); }); it('should fetch project-specific issue types', async () => { const issueTypes = [mockJiraIssueType]; mockedMakeJiraRequest.mockResolvedValue(issueTypes); const result = await getIssueTypes('TEST'); expect(mockedMakeJiraRequest).toHaveBeenCalledWith({ method: 'GET', url: '/project/TEST/issuetype', }); expect(result).toEqual(issueTypes); }); }); describe('getUsers', () => { it('should search users without options', async () => { const users = [mockJiraUser]; mockedMakeJiraRequest.mockResolvedValue(users); const result = await getUsers(); expect(mockedMakeJiraRequest).toHaveBeenCalledWith({ method: 'GET', url: '/user/search', params: {}, }); expect(result).toEqual(users); }); it('should search users with query', async () => { const users = [mockJiraUser]; mockedMakeJiraRequest.mockResolvedValue(users); await getUsers({ query: 'john' }); expect(mockedMakeJiraRequest).toHaveBeenCalledWith({ method: 'GET', url: '/user/search', params: { query: 'john' }, }); }); it('should search users with all options', async () => { const users = [mockJiraUser]; mockedMakeJiraRequest.mockResolvedValue(users); await getUsers({ query: 'john', username: 'john.doe', accountId: 'account-123', startAt: 10, maxResults: 25, }); expect(mockedMakeJiraRequest).toHaveBeenCalledWith({ method: 'GET', url: '/user/search', params: { query: 'john', username: 'john.doe', accountId: 'account-123', startAt: 10, maxResults: 25, }, }); }); }); describe('getPriorities', () => { it('should fetch priorities', async () => { const priorities = [mockJiraPriority]; mockedMakeJiraRequest.mockResolvedValue(priorities); const result = await getPriorities(); expect(mockedMakeJiraRequest).toHaveBeenCalledWith({ method: 'GET', url: '/priority', }); expect(result).toEqual(priorities); }); }); describe('getStatuses', () => { it('should fetch global statuses', async () => { const statuses = [mockJiraStatus]; mockedMakeJiraRequest.mockResolvedValue(statuses); const result = await getStatuses(); expect(mockedMakeJiraRequest).toHaveBeenCalledWith({ method: 'GET', url: '/status', }); expect(result).toEqual(statuses); }); it('should fetch project-specific statuses', async () => { const response = [{ name: 'Bug', statuses: [mockJiraStatus] }]; mockedMakeJiraRequest.mockResolvedValue(response); const result = await getStatuses({ projectKey: 'TEST' }); expect(mockedMakeJiraRequest).toHaveBeenCalledWith({ method: 'GET', url: '/project/TEST/statuses', }); expect(result).toEqual([mockJiraStatus]); }); it('should fetch statuses for project and issue type', async () => { const response = [{ name: 'Bug', statuses: [mockJiraStatus] }]; mockedMakeJiraRequest.mockResolvedValue(response); await getStatuses({ projectKey: 'TEST', issueTypeId: 'bug-id' }); expect(mockedMakeJiraRequest).toHaveBeenCalledWith({ method: 'GET', url: '/project/TEST/statuses', }); }); }); describe('addComment', () => { it('should add comment without visibility', async () => { mockedMakeJiraRequest.mockResolvedValue(mockJiraComment); const result = await addComment('TEST-123', 'This is a comment'); expect(mockedMakeJiraRequest).toHaveBeenCalledWith({ method: 'POST', url: '/issue/TEST-123/comment', data: { body: 'This is a comment' }, }); expect(result).toEqual(mockJiraComment); }); it('should add comment with visibility', async () => { mockedMakeJiraRequest.mockResolvedValue(mockJiraComment); const visibility = { type: 'group', value: 'jira-developers' }; await addComment('TEST-123', 'Private comment', visibility); expect(mockedMakeJiraRequest).toHaveBeenCalledWith({ method: 'POST', url: '/issue/TEST-123/comment', data: { body: 'Private comment', visibility: { type: 'group', value: 'jira-developers' }, }, }); }); }); describe('getProjectDetails', () => { it('should fetch project details without expand', async () => { mockedMakeJiraRequest.mockResolvedValue(mockJiraProjectDetails); const result = await getProjectDetails('TEST'); expect(mockedMakeJiraRequest).toHaveBeenCalledWith({ method: 'GET', url: '/project/TEST', params: {}, }); expect(result).toEqual(mockJiraProjectDetails); }); it('should fetch project details with expand', async () => { mockedMakeJiraRequest.mockResolvedValue(mockJiraProjectDetails); await getProjectDetails('TEST', ['lead', 'description']); expect(mockedMakeJiraRequest).toHaveBeenCalledWith({ method: 'GET', url: '/project/TEST', params: { expand: 'lead,description' }, }); }); }); describe('createSubtask', () => { it('should create subtask with required fields', async () => { mockedMakeJiraRequest .mockResolvedValueOnce(mockJiraIssue) // getIssue for parent .mockResolvedValueOnce([mockJiraIssueType, mockJiraSubtaskIssueType]) // getIssueTypes .mockResolvedValueOnce(mockJiraCreateIssueResponse) // createIssue .mockResolvedValueOnce(mockJiraIssue); // getIssue for created subtask const subtaskData = { summary: 'Test subtask', }; const result = await createSubtask('TEST-123', subtaskData); expect(mockedMakeJiraRequest).toHaveBeenNthCalledWith(1, { method: 'GET', url: '/issue/TEST-123', params: {}, }); expect(mockedMakeJiraRequest).toHaveBeenNthCalledWith(2, { method: 'GET', url: '/project/TEST/issuetype', }); expect(mockedMakeJiraRequest).toHaveBeenNthCalledWith(3, { method: 'POST', url: '/issue', data: { fields: { project: { key: 'TEST' }, parent: { key: 'TEST-123' }, summary: 'Test subtask', issuetype: { id: 'subtask-issue-type-id' }, }, }, }); expect(result).toEqual(mockJiraIssue); }); it('should create subtask with all optional fields', async () => { mockedMakeJiraRequest .mockResolvedValueOnce(mockJiraIssue) .mockResolvedValueOnce([mockJiraSubtaskIssueType]) .mockResolvedValueOnce(mockJiraCreateIssueResponse) .mockResolvedValueOnce(mockJiraIssue); const subtaskData = { summary: 'Test subtask', description: 'Subtask description', priority: 'High', assignee: 'user-123', labels: ['subtask'], components: ['Frontend'], }; await createSubtask('TEST-123', subtaskData); const thirdCall = mockedMakeJiraRequest.mock.calls[2][0]; expect(thirdCall).toMatchObject({ method: 'POST', url: '/issue' }); expect(thirdCall.data.fields).toMatchObject({ project: { key: 'TEST' }, parent: { key: 'TEST-123' }, summary: 'Test subtask', issuetype: { id: 'subtask-issue-type-id' }, priority: { name: 'High' }, assignee: { accountId: 'user-123' }, labels: ['subtask'], components: [{ name: 'Frontend' }], }); expect(thirdCall.data.fields.description).toMatchObject({ type: 'doc', version: 1 }); }); it('should throw error when no subtask issue type found', async () => { mockedMakeJiraRequest .mockResolvedValueOnce(mockJiraIssue) .mockResolvedValueOnce([mockJiraIssueType]); // no subtask type const subtaskData = { summary: 'Test subtask' }; await expect(createSubtask('TEST-123', subtaskData)).rejects.toThrow( 'No subtask issue type found for project TEST' ); }); it('should handle empty arrays for labels and components', async () => { mockedMakeJiraRequest .mockResolvedValueOnce(mockJiraIssue) .mockResolvedValueOnce([mockJiraSubtaskIssueType]) .mockResolvedValueOnce(mockJiraCreateIssueResponse) .mockResolvedValueOnce(mockJiraIssue); const subtaskData = { summary: 'Test subtask', labels: [], components: [], }; await createSubtask('TEST-123', subtaskData); const callData = mockedMakeJiraRequest.mock.calls[2][0].data; expect(callData.fields.labels).toBeUndefined(); expect(callData.fields.components).toBeUndefined(); }); }); });

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