get-issue.test.tsā¢13.8 kB
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { handleGetIssue, getIssueTool } from '../../../src/tools/get-issue.js';
import { validateInput, extractIssueKey } from '../../../src/utils/validators.js';
import { getIssue } from '../../../src/utils/api-helpers.js';
import { formatIssueResponse } from '../../../src/utils/formatters.js';
import { handleError } from '../../../src/utils/error-handler.js';
import { mockJiraIssue, mockNotFoundError } 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 mockedExtractIssueKey = vi.mocked(extractIssueKey);
const mockedGetIssue = vi.mocked(getIssue);
const mockedFormatIssueResponse = vi.mocked(formatIssueResponse);
const mockedHandleError = vi.mocked(handleError);
describe('get-issue tool', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    // Default behavior: extractIssueKey returns the input as-is (for plain issue keys)
    mockedExtractIssueKey.mockImplementation((input) => {
      // Simulate real behavior: extract from URL or return null
      const match = input.match(/[A-Z][A-Z0-9]*-\d+/);
      return match ? match[0] : null;
    });
  });
  describe('getIssueTool configuration', () => {
    it('should have correct tool configuration', () => {
      expect(getIssueTool.name).toBe(TOOL_NAMES.GET_ISSUE);
      expect(typeof getIssueTool.description).toBe('string');
      expect(getIssueTool.description.toLowerCase()).toContain('issue');
      expect(getIssueTool.inputSchema.type).toBe('object');
      expect(getIssueTool.inputSchema.required).toEqual(['issueKey']);
      expect(getIssueTool.inputSchema.properties.issueKey).toBeDefined();
      expect(getIssueTool.inputSchema.properties.expand).toBeDefined();
      expect(getIssueTool.inputSchema.properties.fields).toBeDefined();
    });
  });
  describe('handleGetIssue', () => {
    describe('Success Cases', () => {
      it('should handle basic issue retrieval', async () => {
        const input = { issueKey: 'TEST-123' };
        const validatedInput = { issueKey: 'TEST-123' };
        const mockResponse = { content: [{ type: 'text', text: 'formatted issue' }] };
        mockedValidateInput.mockReturnValue(validatedInput);
        mockedGetIssue.mockResolvedValue(mockJiraIssue);
        mockedFormatIssueResponse.mockReturnValue(mockResponse);
        const result = await handleGetIssue(input);
        expect(mockedValidateInput).toHaveBeenCalledWith(expect.any(Object), input);
        expect(mockedGetIssue).toHaveBeenCalledWith('TEST-123', {});
        expect(mockedFormatIssueResponse).toHaveBeenCalledWith(mockJiraIssue);
        expect(result).toEqual(mockResponse);
      });
      it('should extract issue key from Jira URL', async () => {
        const input = { issueKey: 'https://example.atlassian.net/browse/PROJ-456' };
        const validatedInput = { issueKey: 'https://example.atlassian.net/browse/PROJ-456' };
        const mockResponse = { content: [{ type: 'text', text: 'formatted issue' }] };
        mockedValidateInput.mockReturnValue(validatedInput);
        mockedGetIssue.mockResolvedValue(mockJiraIssue);
        mockedFormatIssueResponse.mockReturnValue(mockResponse);
        const result = await handleGetIssue(input);
        expect(mockedValidateInput).toHaveBeenCalledWith(expect.any(Object), input);
        expect(mockedExtractIssueKey).toHaveBeenCalledWith(
          'https://example.atlassian.net/browse/PROJ-456'
        );
        expect(mockedGetIssue).toHaveBeenCalledWith('PROJ-456', {});
        expect(mockedFormatIssueResponse).toHaveBeenCalledWith(mockJiraIssue);
        expect(result).toEqual(mockResponse);
      });
      it('should extract issue key from Jira URL with query parameters', async () => {
        const input = {
          issueKey:
            'https://example.atlassian.net/jira/software/projects/ABC/boards/1?selectedIssue=ABC-789',
        };
        const validatedInput = {
          issueKey:
            'https://example.atlassian.net/jira/software/projects/ABC/boards/1?selectedIssue=ABC-789',
        };
        const mockResponse = { content: [{ type: 'text', text: 'formatted issue' }] };
        mockedValidateInput.mockReturnValue(validatedInput);
        mockedGetIssue.mockResolvedValue(mockJiraIssue);
        mockedFormatIssueResponse.mockReturnValue(mockResponse);
        const result = await handleGetIssue(input);
        expect(mockedValidateInput).toHaveBeenCalledWith(expect.any(Object), input);
        expect(mockedExtractIssueKey).toHaveBeenCalledWith(
          'https://example.atlassian.net/jira/software/projects/ABC/boards/1?selectedIssue=ABC-789'
        );
        expect(mockedGetIssue).toHaveBeenCalledWith('ABC-789', {});
        expect(mockedFormatIssueResponse).toHaveBeenCalledWith(mockJiraIssue);
        expect(result).toEqual(mockResponse);
      });
      it('should handle issue retrieval with expand options', async () => {
        const input = {
          issueKey: 'TEST-123',
          expand: ['comments', 'attachments'],
        };
        const validatedInput = {
          issueKey: 'TEST-123',
          expand: ['comments', 'attachments'],
        };
        const mockResponse = { content: [{ type: 'text', text: 'formatted issue' }] };
        mockedValidateInput.mockReturnValue(validatedInput);
        mockedGetIssue.mockResolvedValue(mockJiraIssue);
        mockedFormatIssueResponse.mockReturnValue(mockResponse);
        await handleGetIssue(input);
        expect(mockedGetIssue).toHaveBeenCalledWith('TEST-123', {
          expand: ['comments', 'attachments'],
        });
      });
      it('should handle issue retrieval with specific fields', async () => {
        const input = {
          issueKey: 'TEST-123',
          fields: ['summary', 'status'],
        };
        const validatedInput = {
          issueKey: 'TEST-123',
          fields: ['summary', 'status'],
        };
        mockedValidateInput.mockReturnValue(validatedInput);
        mockedGetIssue.mockResolvedValue(mockJiraIssue);
        mockedFormatIssueResponse.mockReturnValue({ content: [] });
        await handleGetIssue(input);
        expect(mockedGetIssue).toHaveBeenCalledWith('TEST-123', {
          fields: ['summary', 'status'],
        });
      });
      it('should handle issue retrieval with both expand and fields', async () => {
        const input = {
          issueKey: 'TEST-123',
          expand: ['comments'],
          fields: ['summary', 'status'],
        };
        const validatedInput = {
          issueKey: 'TEST-123',
          expand: ['comments'],
          fields: ['summary', 'status'],
        };
        mockedValidateInput.mockReturnValue(validatedInput);
        mockedGetIssue.mockResolvedValue(mockJiraIssue);
        mockedFormatIssueResponse.mockReturnValue({ content: [] });
        await handleGetIssue(input);
        expect(mockedGetIssue).toHaveBeenCalledWith('TEST-123', {
          expand: ['comments'],
          fields: ['summary', 'status'],
        });
      });
      it('should handle undefined expand and fields gracefully', async () => {
        const validatedInput = {
          issueKey: 'TEST-123',
          expand: undefined,
          fields: undefined,
        };
        mockedValidateInput.mockReturnValue(validatedInput);
        mockedGetIssue.mockResolvedValue(mockJiraIssue);
        mockedFormatIssueResponse.mockReturnValue({ content: [] });
        await handleGetIssue({});
        expect(mockedGetIssue).toHaveBeenCalledWith('TEST-123', {});
      });
    });
    describe('Validation', () => {
      it('should validate input using schema', async () => {
        const input = { issueKey: 'TEST-123' };
        mockedValidateInput.mockReturnValue(input);
        mockedGetIssue.mockResolvedValue(mockJiraIssue);
        mockedFormatIssueResponse.mockReturnValue({ content: [] });
        await handleGetIssue(input);
        expect(mockedValidateInput).toHaveBeenCalledWith(
          expect.objectContaining({
            _def: expect.objectContaining({
              typeName: 'ZodObject',
            }),
          }),
          input
        );
      });
      it('should handle validation errors', async () => {
        const input = { issueKey: '' };
        const validationError = new Error('Validation failed: issueKey is required');
        const mockErrorResponse = { content: [{ type: 'text', text: 'Validation error' }] };
        mockedValidateInput.mockImplementation(() => {
          throw validationError;
        });
        mockedHandleError.mockReturnValue(mockErrorResponse);
        const result = await handleGetIssue(input);
        expect(mockedHandleError).toHaveBeenCalledWith(validationError);
        expect(result).toEqual(mockErrorResponse);
      });
    });
    describe('Error Handling', () => {
      it('should handle API errors from getIssue', async () => {
        const input = { issueKey: 'TEST-123' };
        const mockErrorResponse = { content: [{ type: 'text', text: 'API error' }] };
        mockedValidateInput.mockReturnValue(input);
        mockedGetIssue.mockRejectedValue(mockNotFoundError);
        mockedHandleError.mockReturnValue(mockErrorResponse);
        const result = await handleGetIssue(input);
        expect(mockedHandleError).toHaveBeenCalledWith(mockNotFoundError);
        expect(result).toEqual(mockErrorResponse);
      });
      it('should handle network errors', async () => {
        const input = { issueKey: 'TEST-123' };
        const networkError = new Error('Network Error');
        networkError.code = 'ECONNREFUSED';
        const mockErrorResponse = { content: [{ type: 'text', text: 'Network error' }] };
        mockedValidateInput.mockReturnValue(input);
        mockedGetIssue.mockRejectedValue(networkError);
        mockedHandleError.mockReturnValue(mockErrorResponse);
        const result = await handleGetIssue(input);
        expect(mockedHandleError).toHaveBeenCalledWith(networkError);
        expect(result).toEqual(mockErrorResponse);
      });
      it('should handle authentication errors', async () => {
        const input = { issueKey: 'TEST-123' };
        const authError = { response: { status: 401, data: { errorMessages: ['Unauthorized'] } } };
        const mockErrorResponse = { content: [{ type: 'text', text: 'Auth error' }] };
        mockedValidateInput.mockReturnValue(input);
        mockedGetIssue.mockRejectedValue(authError);
        mockedHandleError.mockReturnValue(mockErrorResponse);
        const result = await handleGetIssue(input);
        expect(mockedHandleError).toHaveBeenCalledWith(authError);
        expect(result).toEqual(mockErrorResponse);
      });
      it('should handle unexpected errors', async () => {
        const input = { issueKey: 'TEST-123' };
        const unexpectedError = new Error('Something went wrong');
        const mockErrorResponse = { content: [{ type: 'text', text: 'Unexpected error' }] };
        mockedValidateInput.mockReturnValue(input);
        mockedGetIssue.mockRejectedValue(unexpectedError);
        mockedHandleError.mockReturnValue(mockErrorResponse);
        const result = await handleGetIssue(input);
        expect(mockedHandleError).toHaveBeenCalledWith(unexpectedError);
        expect(result).toEqual(mockErrorResponse);
      });
    });
    describe('Edge Cases', () => {
      it('should handle empty input object', async () => {
        const input = {};
        const validationError = new Error('issueKey is required');
        const mockErrorResponse = { content: [{ type: 'text', text: 'Validation error' }] };
        mockedValidateInput.mockImplementation(() => {
          throw validationError;
        });
        mockedHandleError.mockReturnValue(mockErrorResponse);
        const result = await handleGetIssue(input);
        expect(result).toEqual(mockErrorResponse);
      });
      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 handleGetIssue(null);
        expect(result).toEqual(mockErrorResponse);
      });
      it('should handle issue key with whitespace', async () => {
        const input = { issueKey: '  TEST-456  ' };
        const validatedInput = { issueKey: '  TEST-456  ' };
        mockedValidateInput.mockReturnValue(validatedInput);
        mockedGetIssue.mockResolvedValue(mockJiraIssue);
        mockedFormatIssueResponse.mockReturnValue({ content: [] });
        await handleGetIssue(input);
        expect(mockedGetIssue).toHaveBeenCalledWith('TEST-456', {});
      });
      it('should handle very long expand and fields arrays', async () => {
        const input = {
          issueKey: 'TEST-123',
          expand: Array(50).fill('comments'),
          fields: Array(100).fill('summary'),
        };
        mockedValidateInput.mockReturnValue(input);
        mockedGetIssue.mockResolvedValue(mockJiraIssue);
        mockedFormatIssueResponse.mockReturnValue({ content: [] });
        await handleGetIssue(input);
        expect(mockedGetIssue).toHaveBeenCalledWith('TEST-123', {
          expand: Array(50).fill('comments'),
          fields: Array(100).fill('summary'),
        });
      });
    });
  });
});